Cucumber-js: Proposta: API programática para executar o cucumber-js

Criado em 28 jun. 2021  ·  29Comentários  ·  Fonte: cucumber/cucumber-js

Problema

Atualmente não temos uma boa maneira de executar o cucumber-js de maneira programática. A necessidade é de dois ângulos:

  • Testing cucumber-js - para o CCK e nossos testes de aceitação no projeto
  • Teste de formatadores e snippets personalizados
  • Projetos que usam cucumber-js como parte de uma estrutura (por exemplo, Serenity, Stryker)

Como é

O que tende a acontecer no momento é que uma nova instância de Cli seja criada com a entrada argv amarrada . Obviamente, é muito pesado e também não está na API pública .

Às vezes (possivelmente devido à fragilidade percebida do acima), as estruturas contarão apenas com a CLI do cucumber-js, mas terão dificuldade para encontrar maneiras de integrar e ter suas próprias opções.

A classe Runtime atualmente faz parte da API pública, mas não é útil nesses contextos, dependendo dos pickles e do código de suporte a ser fornecido pelo chamador.

Proposta

Dois componentes do projeto:

runCucumber

Nova função assíncrona que executa uma execução de teste no processo. Responsabilidades:

  • Aceite um objeto de opções com uma interface agradável
  • Faça o "trabalho" de resolver pickles, carregar código de suporte, executar casos de teste, orquestrar formatadores
  • Devolva uma promessa que leva a um resultado

Isso seria parte da API pública e encorajamos os mantenedores do framework a usá-lo ao "embrulhar" o cucumber-js. Também o usaríamos para nossos próprios testes.

Tanto quanto possível, isso evitaria a interação direta com process , em vez de aceitar opções normalizadas e interfaces de fluxo para saída e deixar para o chamador decidir como sair com base no resultado ou em um erro não tratado.

Além disso, Runtime deve sair da API pública, pois é na verdade uma coisa interna.

CLI

Efetivamente, um "cliente" de runCucumber . Responsabilidades:

  • Agregar opções de várias fontes (argv, env vars, arquivos de configuração) (veja o comentário )
  • Ligue para runCucumber com as opções resolvidas
  • Saia conforme apropriado com base nos resultados

Isso continuaria a não estar na API pública. Além disso, ele usaria apenas funções / interfaces que estão na API pública, de forma que poderíamos facilmente dividi-lo em seu próprio pacote em algum ponto, como é um padrão comum agora em projetos como Jest .

Essa dissociação também abre caminho para alguns novos recursos interessantes da CLI sem que eles vazem para o interior, por exemplo:

  • --gui para as coisas de pepino-elétron
  • --interactive para reexecuções direcionadas rápidas ao realizar TDD

etc

Também exporíamos funções (consumíveis pela CLI e por outros) para:

  • Obtendo as opções
  • Lidando com i18nKeywords e i18nLanguages

Escala de tempo

Pretendemos isso na próxima versão 8.0.0. Estou pronto para começar hoje.

breaking change enhancement

Comentários muito úteis

Como isso funcionaria para um repórter que não precisa de um nome de arquivo de saída?

formats: {
    stdout: './my-awesome-stryker-formatter',
    files: {
      'report.html': './my/fancy-reporter.js',
      'other-report.html': '@me/reporter-package',
    }
  },

(apenas um formatador pode usar o fluxo stdout)

Todos 29 comentários

Paginação para feedback inicial: @aslakhellesoy @charlierudolph @ aurelien-reeves @mattwynne @nicojs @ jan-molak

Eu amo essa proposta! Já temos uma API como esta no runCucumber de fake-cucumber

@davidjgoss - parece ótimo!

Para sua referência, veja como Serenity / JS invoca Cucumber no momento - CucumberCLIAdapter
E aqui está a lógica em torno da conversão dos parâmetros de configuração em argv - CucumberOptions .

Ser capaz de fornecer um objeto de opções em vez de argv seria muito melhor 👍🏻

Adoro!

Ao especificar essa nova API pública, também podemos considerar o que aconteceu recentemente com o problema nº 1489 e pensar em fornecer APIs públicas para ter mais e melhor interação com os filtros e os recursos resultantes em teste

Ter uma API pública é melhor do que nada, então vá em frente 👍!

De preferência, eu também teria uma API para carregar perfis usando as mesmas regras de cucumber-js , para que eu possa imitar o comportamento exato de uma chamada normal de cucumber-js.

loadProfiles(directory = process.cwd()): Record<string, Profile>

StrykerJS também dependerá fortemente da API custom_formatters e dos eventos publicados pelo eventBroadcaster . Podemos adicioná-los à API pública também? Consulte: https://github.com/stryker-mutator/stryker-js/blob/03b1f20ed933d3a50b52022cfe363c606c2b16c5/packages/cucumber-runner/src/stryker-formatter.ts#L45 -L69

De preferência, eu também teria uma API para carregar perfis usando as mesmas regras que o cucumber-js faz, para que eu possa imitar o comportamento exato de uma chamada normal de cucumber-js.

Este é um bom ponto. Os perfis (pelo menos em sua forma atual) são fundamentalmente acoplados à CLI, por isso parece certo mantê-los daquele lado do limite, mas ainda poderíamos expor uma função para carregá-los e gerar um objeto de opções parciais.

Ao especificar essa nova API pública, também podemos considerar o que aconteceu recentemente com o problema nº 1489 e pensar em fornecer APIs públicas para ter mais e melhor interação com os filtros e os recursos resultantes em teste.

Acho que poderíamos incluir uma opção para fornecer um filtro de pickle personalizado ao chamar a API (além dos nomes, tags etc. que conduzem a filtragem embutida).

A sintaxe atual para perfis é muito baseada na linha de comando.

Eu ❤️❤️❤️ seria capaz de especificar perfis em um formato mais genérico, como JSON, JavaScript, YAML ou variáveis ​​de ambiente. Em JSON, poderia ser assim:

.cucumber.json

{
  "default": {
    "requireModule": ["ts-node/register"],
    "require": ["support/**/*./ts"],
    "worldParameters": {
      "appUrl": "http://localhost:3000/",
    },
    "format": ["progress-bar", "html:./cucumber-report.html"]
  },
  "ci": {
    "requireModule": ["ts-node/register"],
    "require": ["support/**/*./ts"],
    "worldParameters": {
      "appUrl": "http://localhost:3000/",
    },
    "format": ["html:./cucumber-report.html"],
    "publish": true
  }
}

Ou, usando JavaScript

.cucumber.js

const common = {
  "requireModule": ["ts-node/register"],
  "require": ["support/**/*./ts"],
  "worldParameters": {
    "appUrl": "http://localhost:3000/",
  }
}

module.exports = {
  default: {
    ...common,
    "format": ["progress-bar", "html:./cucumber-report.html"]
  },
  ci: {
    ...common,
    "format": ["html:./cucumber-report.html"],
    "publish": true
  }
}

Ou mesmo com variáveis ​​de ambiente (por exemplo, carregado com uma ferramenta como o dotenv):

.cucumber.env

CUCUMBER_PROFILE_DEFAULT_REQUIREMODULE=ts-node/register
CUCUMBER_PROFILE_DEFAULT_REQUIRE=ts-node/register
CUCUMBER_PROFILE_DEFAULT_WORLDPARAMETERS_APPURL=http://localhost:3000/
CUCUMBER_PROFILE_DEFAULT_FORMAT=progress-bar,html:./cucumber-report.html
CUCUMBER_PROFILE_CI_REQUIREMODULE=ts-node/register
CUCUMBER_PROFILE_CI_REQUIRE=ts-node/register
CUCUMBER_PROFILE_CI_WORLDPARAMETERS_APPURL=http://localhost:3000/
CUCUMBER_PROFILE_CI_FORMAT=progress-bar,html:./cucumber-report.html
CUCUMBER_PROFILE_CI_PUBLISH=true

Na verdade, a biblioteca de configuração faz exatamente isso. Nunca acabamos integrando-o no Cucumber-JVM porque outras coisas atrapalharam, mas talvez pudéssemos tentar com uma implementação de JavaScript?

@aslakhellesoy concorda que seria incrível! Vou tentar conseguir um POC para esta proposta, então temos algo um pouco mais concreto para conversar e adoraríamos fazer perfis certos como parte dela (4,5 anos e contando com # 751 😄)

Refs. # 1004

Este é um bom ponto. Os perfis (pelo menos em sua forma atual) são fundamentalmente acoplados à CLI, por isso parece certo mantê-los daquele lado do limite, mas ainda poderíamos expor uma função para carregá-los e gerar um objeto de opções parciais.

Sim, isso seria incrível e muito apreciado do ponto de vista dos criadores de plug-ins.

Eu ❤️❤️❤️ seria capaz de especificar perfis em um formato mais genérico, como JSON, JavaScript, YAML ou variáveis ​​de ambiente. Em JSON, poderia ser assim:

Isso parece ótimo! E também é exatamente a razão pela qual eu gostaria que uma API os carregasse da mesma forma que o cucumberJS faz. Carregar um único arquivo cucumber.js é trivial. Replicar um algoritmo de carregamento de arquivo de configuração, incluindo precedência, formato de arquivo, etc. E mantê-lo é algo totalmente diferente 😅.

P: Eu seria capaz de executar runCucumber duas vezes consecutivas _sem limpar o cache necessário _? Isso é importante para o caso de uso de teste de mutação.

Queremos carregar o ambiente e executar os testes várias vezes em rápida sucessão enquanto alteramos uma variável global para alternar o mutante ativo.

No momento, estamos usando a API privada cli e precisamos limpar os arquivos de definição de etapa de require.cache entre cada execução de teste. Isso não é ideal para CommonJS e nem funcionará para ESM.

Pseudocódigo do nosso caso de uso:

const profiles = await loadProfiles();
const options = {
  ...profiles.default,
  formatter: require.resolve('./our-awesomely-crafted-formatter'),
  some: 'other options we want to override',
}
const cucumber = new Cucumber(options);

// Allow cucumber to load the step definitions once. 
// This is `async`, so support for esm can be added without a breaking change
await cucumber.initialize();

// Initial test run ("dry run"), without mutants active
await cucumber.run();

collectMutantCoveragePerTestFromFormatter();

// Start mutation testing:

global.activeMutant = 1;
await cucumber.run({ features: ['features/a.feature:24']);
collectResultsFromFormatterToDetermineKilledOrSurvivedMutant()

global.activeMutant = 2;
await cucumber.run({ features: ['features/b.feature:24:25:26', 'features/c.feature:12']);
collectResultsFromFormatterToDetermineKilledOrSurvivedMutant()

// etc

@nicojs definitivamente concorda que precisamos disso, já apareceu algumas vezes antes, por exemplo, com pessoas que queriam executar pepino em um lambda, e eu também gostaria de adicionar um modo interativo que também precisaria disso.

O que eu esbocei até agora foi em um estilo mais funcional, mas o mesmo conceito fundamental, eu acho:

const runnerOptions = {
    support: {
        require: ['features/support/**/*.js']
    }
}

// initial run returns support code library
const { support } = await runCucumber(runnerOptions)

// subsequent run reuses support code library
await runCucumber({
    ...runnerOptions,
    support
})

Isso funciona para nós 👍

Funciona para nós também 👍🏻

Eu realmente acho que algo assim seria extremamente útil como uma alternativa para a grande confusão que temos hoje com integrações de ferramentas de teste (Jest, Cypress), por exemplo, encontrei estes problemas (em ordem de importância):

  • cypress-cucumber-preprocessor não oferece suporte a tags em exemplos (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/196)
  • jest-cucumber não suporta relatórios Cucumber JSON (https://github.com/bencompton/jest-cucumber/issues/27)
  • cypress-cucumber-preprocessor gera vários relatórios Cucumber JSON sem suporte oficial para agregação (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/423)
  • jest-cucumber não é tão conveniente quanto jest-cucumber-fusion
  • também há cucumber-jest ...
  • Karma não tem mais uma implementação funcional (https://github.com/cucumber/cucumber-js/issues/1095)

Eu preferiria ver algum código de cola mínimo entre Jest / Karma / Cypress / etc. e pepino-js para que eu não precise sofrer por todos aqueles recursos ausentes que preciso usar.

Ótima sugestão @davidjgoss 👍

Essa separação de interesses entre a interface de usuário da linha de comando e a "lógica de negócios" de análise e execução de cenários como testes me lembra o padrão de arquitetura hexagonal .

No pepino-rubi, nós realmente dividimos a lógica do domínio central (ou "hexágono interno") em um pacote de gemas separado, enquanto o estávamos reconstruindo do zero em uma "sala limpa". Sei que esse não é o contexto aqui, mas pode valer a pena inspirar-se ou alimentar as inovações desse design na API Ruby. Há um exemplo no README do pepino-rubi-núcleo gem de como usar essa API.

Ok, aqui está uma primeira passagem na assinatura da API para o bit "run". É fortemente baseado no objeto IConfiguration que temos internamente (então não deve causar muita refatoração na parte inferior), mas apenas um pouco menos "plano":

export interface IRunCucumberOptions {
  cwd: string
  features: {
    defaultDialect?: string
    paths: string[]
  }
  filters: {
    name?: string[]
    tagExpression?: string
  }
  support:
    | {
        transpileWith?: string[]
        paths: string[]
      }
    | ISupportCodeLibrary
  runtime: {
    dryRun?: boolean
    failFast?: boolean
    filterStacktraces?: boolean
    parallel?: {
      count: number
    }
    retry?: {
      count: number
      tagExpression?: string
    }
    strict: boolean
    worldParameters?: any
  }
  formats: {
    stdout: string
    files: Record<string, string>
    options: IParsedArgvFormatOptions
  }
}

export interface IRunResult {
  success: boolean
  support: ISupportCodeLibrary
}

export async function runCucumber(
  options: IRunCucumberOptions
): Promise<IRunResult> {
  // do stuff
}

E um exemplo de uso muito elaborado:

const result = await runCucumber({
  cwd: process.cwd(),
  features: {
    paths: ['features/**/*.feature'],
  },
  filters: {
    name: ['Acme'],
    tagExpression: '<strong i="10">@interesting</strong>',
  },
  support: {
    transpileWith: ['ts-node'],
    paths: ['features/support/**/*.ts'],
  },
  runtime: {
    failFast: true,
    retry: {
      count: 1,
      tagExpression: '<strong i="11">@flaky</strong>',
    },
    strict: true,
    worldParameters: {
      foo: 'bar',
    },
  },
  formats: {
    stdout: '@cucumber/pretty-formatter',
    files: {
      'report.html': 'html',
      'TEST-cucumber.xml': 'junit',
    },
    options: {
      printAttachments: false,
    },
  },
})

Feedback seja bem-vindo! Observe que isso não cobre o carregamento de perfis / configurações, o que seria outra função.

Eu acho que isso parece bom. Pergunta: Como eu configuraria um formatador personalizado?

@nicojs sorta como na CLI

formats: {
    files: {
      'report.html': './my/fancy-reporter.js',
      'other-report.html': '@me/reporter-package',
    }
  },

Ótimo ver o progresso neste @davidjgoss!

Não quero retardar o progresso nisso, mas, ao mesmo tempo, quero ter certeza de que adotaremos um formato que possa funcionar para outras implementações do Cucumber também.

Eventualmente, um esquema JSON, mas enquanto o discutimos, acho que os tipos do TypeScript são mais fáceis de analisar para nós, humanos.

Sugiro que criemos uma nova edição sobre o formato proposto no cucumber/common monorepo e convide a equipe principal para discutir lá.

@aslakhellesoy servirá.

O que você acha sobre a API programática não estar vinculada à estrutura de opções comuns? Como mapearíamos disso para as opções runCucumber . Isso talvez acrescente um pouco de complexidade, mas apela por causa de coisas como ter um bloco support que seja parâmetros para carregar ou uma biblioteca de código de suporte carregada anteriormente. Poderia fazer o mesmo para recursos + picles também. E há várias opções que suportamos na CLI (por exemplo, --exit ) que não são apropriadas na API programática.

O que você acha sobre a API programática não estar vinculada à estrutura de opções comuns?

Acho que está tudo bem, contanto que forneçamos uma função que converta o conteúdo do arquivo de opções para a estrutura de dados runCucumber deseja.

Eventualmente, um esquema JSON, mas enquanto o discutimos, acho que os tipos do TypeScript são mais fáceis de analisar para nós, humanos.

Por que precisamos escolher? Estamos usando o esquema JSON no StrykerJS para gerar o typescript usando json-schema-to-typescript . Não estamos enviando os arquivos de saída TS para o controle de origem, em vez disso, os geramos em tempo real usando uma etapa prebuild .

Os esquemas JSON ainda são legíveis para humanos IMO. Já tivemos relações públicas no repositório da Stryker e as pessoas parecem saber o que fazer 🤷‍♀️

mais ou menos como no CLI

formats: {
    files: {
      'report.html': './my/fancy-reporter.js',
      'other-report.html': '@me/reporter-package',
    }
  },

Como isso funcionaria para um repórter que não precisa de um nome de arquivo de saída? Igual a:

formats: {
  files: {
    '': require.resolve('./my-awesome-stryker-formatter')
  }
}

Por que precisamos escolher?

Acho que devemos usar um esquema JSON como uma única fonte de verdade para a estrutura da configuração. -E então gere o código TypeScript / Java / Whatever a partir desse esquema.

Mas o esquema JSON é um pouco difícil de ler para humanos, então, enquanto discutimos o esquema em um problema do GitHub em cucumber/common , sugeri TypeScript para facilitar a discussão.

Veja o que quero dizer?

Os esquemas JSON ainda são legíveis para humanos IMO

Não para mim :-) Muito prolixo.

Como isso funcionaria para um repórter que não precisa de um nome de arquivo de saída?

formats: {
    stdout: './my-awesome-stryker-formatter',
    files: {
      'report.html': './my/fancy-reporter.js',
      'other-report.html': '@me/reporter-package',
    }
  },

(apenas um formatador pode usar o fluxo stdout)

Esta página foi útil?
0 / 5 - 0 avaliações