Cucumber-js: Proposition : API programmatique pour exécuter cucumber-js

Créé le 28 juin 2021  ·  29Commentaires  ·  Source: cucumber/cucumber-js

Problème

Actuellement, nous n'avons pas un bon moyen d'exécuter par programme cucumber-js. Le besoin se présente sous deux angles :

  • Test cucumber-js - pour le CCK et nos tests d'acceptation dans le projet
  • Tester des formateurs personnalisés et des extraits de code
  • Projets qui utilisent cucumber-js dans le cadre d'un framework (par exemple Serenity, Stryker)

Comme si

Ce qui a tendance à se produire en ce moment, c'est qu'une nouvelle instance de Cli est créée avec une entrée argv enchaînée . C'est évidemment très peu pratique et n'est pas non plus sur l'API publique .

Parfois (peut-être en raison de la fragilité perçue de ce qui précède), les frameworks s'appuieront uniquement sur la CLI cucumber-js, mais auront du mal à trouver des moyens d'intégrer et d'avoir leurs propres options.

La classe Runtime fait actuellement partie de l'API publique mais elle n'est pas utile dans ces contextes, selon les pickles et le code de support à fournir par l'appelant.

Proposition

Deux volets dans le projet :

runCucumber

Nouvelle fonction asynchrone qui exécute un test en cours de processus. Responsabilités:

  • Accepter un objet d'options avec une belle interface
  • Faire le "travail" de résolution des pickles, de chargement du code de support, d'exécution de cas de test, d'orchestration des formateurs
  • Renvoyer une promesse qui se résout à un résultat

Cela ferait partie de l'API publique et nous encourageons les responsables du framework à l'utiliser lors de l'« emballage » de cucumber-js. Nous l'utiliserions également pour nos propres tests.

Autant que possible, cela éviterait une interaction directe avec process , en acceptant plutôt des options normalisées et des interfaces de flux pour la sortie, et en laissant à l'appelant le soin de décider comment quitter en fonction du résultat ou d'une erreur non gérée.

De plus, Runtime devrait sortir de l'API publique car c'est vraiment une chose interne.

CLI

Effectivement un "client" de runCucumber . Responsabilités:

  • Agréger les options de diverses sources (argv, env vars, fichiers de configuration) (voir commentaire )
  • Appelez runCucumber avec les options résolues
  • Quitter le cas échéant en fonction des résultats

Cela continuerait à ne pas être sur l'API publique. De plus, il n'utiliserait que des fonctions/interfaces qui se trouvent sur l'API publique, de sorte que nous pourrions facilement le diviser en son propre package à un moment donné, comme c'est un modèle courant maintenant avec des projets comme Jest .

Ce découplage ouvre également la voie à de nouvelles fonctionnalités intéressantes de la CLI sans qu'elles ne se répercutent sur les composants internes, par exemple :

  • --gui pour les trucs concombre-électrons
  • --interactive pour des rediffusions ciblées rapides lors du TDD

etc

On exposerait aussi des fonctions (consommables par la CLI et par d'autres) pour :

  • Obtenir les options
  • Manipulation i18nKeywords et i18nLanguages

Échelle de temps

Nous ciblerons cela dans la prochaine version 8.0.0. Je suis prêt à commencer aujourd'hui.

breaking change enhancement

Commentaire le plus utile

Comment cela fonctionnerait-il pour un journaliste qui n'a pas besoin d'un nom de fichier de sortie ?

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

(un seul formateur peut utiliser le flux stdout)

Tous les 29 commentaires

Pagination pour les premiers commentaires : @aslakhellesoy @charlierudolph @aurelien-reeves @mattwynne @nicojs @jan-molak

J'adore cette proposition ! Nous avons déjà une API comme celle-ci dans runCucumber de faux-concombre

@davidjgoss -

Pour votre information, voici comment Serenity/JS invoque le concombre en ce moment - CucumberCLIAdapter
Et voici la logique de la conversion des paramètres de configuration en argv - CucumberOptions .

Pouvoir fournir un objet d'options au lieu de argv serait beaucoup plus agréable 👍🏻

Aimer!

Tout en spécifiant cette nouvelle API publique, nous pouvons également considérer ce qui s'est récemment passé avec le problème #1489 et penser à fournir des API publiques pour avoir une interaction plus importante et meilleure avec les filtres et les fonctionnalités résultantes en cours de test.

Avoir une API publique c'est mieux que rien, alors allez-y !

De préférence, j'aurais également une API pour charger des profils en utilisant les mêmes règles que cucumber-js , afin que je puisse imiter le comportement exact d'un appel normal de concombre-js.

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

StrykerJS s'appuiera également fortement sur l'API custom_formatters et les événements publiés par le eventBroadcaster . Pourrions-nous également les ajouter à l'API publique ? Voir : https://github.com/stryker-mutator/stryker-js/blob/03b1f20ed933d3a50b52022cfe363c606c2b16c5/packages/cucumber-runner/src/stryker-formatter.ts#L45 -L69

De préférence, j'aurais également une API pour charger des profils en utilisant les mêmes règles que cucumber-js, afin que je puisse imiter le comportement exact d'un appel normal de cucumber-js.

C'est un bon point. Les profils (dans leur forme actuelle du moins) sont fondamentalement couplés à la CLI, il semble donc juste de les garder de ce côté de la frontière, mais nous pourrions toujours exposer une fonction pour les charger et générer un objet d'options partiel.

Tout en spécifiant cette nouvelle API publique, nous pouvons également considérer ce qui s'est récemment passé avec le problème #1489 et penser à fournir des API publiques pour avoir une interaction plus importante et meilleure avec les filtres et les fonctionnalités résultantes en cours de test.

Je pense que nous pourrions inclure une option pour fournir un filtre pickle personnalisé lors de l'appel de l'API (en plus des noms, balises, etc. qui pilotent le filtrage intégré).

La syntaxe actuelle pour les profils est très en ligne de commande.

Je voudrais ❤️❤️❤️ pouvoir spécifier des profils dans un format plus générique tel que JSON, JavaScript, YAML ou des variables d'environnement. En JSON, cela pourrait ressembler à ceci :

.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, en utilisant 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 même avec des variables d'environnement (par exemple chargées avec un outil comme 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

En fait, la bibliothèque de configuration fait exactement cela. Nous n'avons jamais fini par l'intégrer dans Cucumber-JVM parce que d'autres éléments nous gênaient, mais peut-être pourrions-nous essayer avec une implémentation JavaScript ?

@aslakhellesoy est d' accord, ce serait génial ! Je vais essayer d'obtenir un POC pour cette proposition afin que nous ayons quelque chose d'un peu plus concret à discuter, et j'adorerais faire des profils dans le cadre de celle-ci (4,5 ans et compte sur #751 😄)

Réf. #1004

C'est un bon point. Les profils (dans leur forme actuelle du moins) sont fondamentalement couplés à la CLI, il semble donc juste de les garder de ce côté de la frontière, mais nous pourrions toujours exposer une fonction pour les charger et générer un objet d'options partiel.

Oui, ce serait génial et très apprécié du point de vue des créateurs de plugins.

Je voudrais ❤️❤️❤️ pouvoir spécifier des profils dans un format plus générique tel que JSON, JavaScript, YAML ou des variables d'environnement. En JSON, cela pourrait ressembler à ceci :

Cela sonne bien ! Et c'est aussi exactement la raison pour laquelle j'apprécierais une API pour les charger de la même manière que cucumberJS. Le chargement d'un seul fichier cucumber.js est trivial. Répliquer un algorithme de chargement de fichier de configuration, y compris la priorité, le format de fichier, etc. ET le maintenir est tout autre chose .

Q : Pourrais-je exécuter runCucumber deux fois de suite _sans effacer le cache requis _ ? Ceci est important pour le cas d'utilisation du test de mutation.

Nous voulons charger l'environnement et exécuter les tests plusieurs fois en succession rapide tout en modifiant une variable globale afin de changer le mutant actif.

À l'heure actuelle, nous utilisons l'API privée cli et nous devons effacer les fichiers de définition d'étape des require.cache entre chaque exécution de test. Ce n'est pas idéal pour CommonJS et ne fonctionnera pas du tout pour esm.

Pseudo code de notre cas d'utilisation :

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 est tout à fait d'accord pour dire que nous en avons besoin, cela est déjà arrivé plusieurs fois avec, par exemple, des personnes souhaitant exécuter du concombre dans un lambda, et j'aimerais également ajouter un mode interactif qui en aurait également besoin.

Ce que j'avais esquissé jusqu'à présent était plus un style fonctionnel mais le même concept fondamental je pense :

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
})

Ça marche pour nous

Fonctionne pour nous aussi 👍🏻

Je pense vraiment que quelque chose comme ça serait extrêmement utile comme alternative au grand désordre que nous avons aujourd'hui avec les intégrations d'outils de test (Jest, Cypress), par exemple j'ai trouvé ces problèmes (par ordre d'importance) :

  • cypress-cucumber-preprocessor ne prend pas en charge les balises sur les exemples (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/196)
  • jest-cucumber ne prend pas en charge les rapports JSON sur le concombre (https://github.com/bencompton/jest-cucumber/issues/27)
  • cypress-cucumber-preprocessor génère plusieurs rapports JSON sur le concombre sans prise en charge officielle de l'agrégation (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/423)
  • jest-cucumber n'est pas aussi pratique que jest-cucumber-fusion
  • il y a aussi cucumber-jest ...
  • Karma n'a plus d'implémentation fonctionnelle (https://github.com/cucumber/cucumber-js/issues/1095)

Je préférerais voir un code de colle minimal entre Jest/Karma/Cypress/etc. et cucumber-js pour que je n'aie pas à souffrir de toutes ces fonctionnalités manquantes que j'ai besoin d'utiliser.

Super suggestion @davidjgoss

Cette séparation des préoccupations entre l'interface utilisateur en ligne de commande et la « logique métier » d'analyse et d'exécution de scénarios sous forme de tests me rappelle le modèle d' architecture hexagonale .

Dans cucumber-ruby, nous avons en fait divisé la logique du domaine central (ou « l'hexagone intérieur ») en un package gem séparé, alors que nous le reconstruisions à partir de zéro dans une « salle blanche ». Je me rends compte que ce n'est pas le contexte ici, mais cela pourrait valoir la peine de s'inspirer ou de renvoyer les innovations de cette conception dans l'API Ruby. Il y a un exemple dans le README de la gem cucumber-ruby-core sur la façon d'utiliser cette API.

D'accord, voici un premier passage à la signature de l'API pour le bit "run". C'est fortement basé sur l'objet IConfiguration nous avons en interne (donc ne devrait pas causer trop de refactorisation plus bas) mais juste un peu moins "plat":

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
}

Et un exemple d'utilisation très artificiel :

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,
    },
  },
})

Commentaires bienvenus! Notez que cela ne couvre pas le chargement de profil/configuration qui serait une autre fonction.

Je pense que cela a l'air bien. Question : Comment configurer un formateur personnalisé ?

@nicojs un peu comme sur la CLI

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

Super de voir des progrès sur ce @davidjgoss !

Je ne veux pas ralentir les progrès sur ce point, mais en même temps, je veux m'assurer que nous adoptons un format qui peut également fonctionner pour d'autres implémentations de Cucumber.

Finalement, un schéma JSON, mais pendant que nous en discutons, je pense que les types TypeScript sont plus faciles à analyser pour nous, les humains.

Je suggère que nous créions un nouveau numéro sur le format proposé dans le monorepo cucumber/common et que nous invitions l'équipe principale à en discuter.

@aslakhellesoy fera l'affaire.

Que penseriez-vous du fait que l'API programmatique n'est pas liée à la structure d'options commune ? Comme si nous utilisions cela pour les options runCucumber . Cela ajoute peut-être un peu de complexité, mais est attrayant en raison de choses comme avoir un bloc support qui est soit des paramètres à charger, soit une bibliothèque de code de support précédemment chargée. Pourrait faire la même chose pour les fonctionnalités + les cornichons aussi. Et il existe diverses options que nous prendrions en charge sur la CLI (par exemple, --exit ) qui ne sont pas appropriées sur l'API de programmation.

Que penseriez-vous du fait que l'API programmatique n'est pas liée à la structure d'options commune ?

Je pense que c'est bien, tant que nous fournissons une fonction qui convertit le contenu du fichier d'options en la structure de données souhaitée par runCucumber .

Finalement, un schéma JSON, mais pendant que nous en discutons, je pense que les types TypeScript sont plus faciles à analyser pour nous, les humains.

Pourquoi devons-nous choisir ? Nous utilisons le schéma JSON dans StrykerJS pour générer un script à l'aide de json-schema-to-typescript . Nous ne soumettons pas les fichiers de sortie TS au contrôle de source, mais nous les générons à la volée en utilisant une étape prebuild .

Les schémas JSON sont encore quelque peu lisibles pour les humains IMO. Nous avons déjà eu des relations publiques sur le repo Stryker et les gens semblent savoir quoi faire 🤷‍♀️

un peu comme sur la CLI

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

Comment cela fonctionnerait-il pour un journaliste qui n'a pas besoin d'un nom de fichier de sortie ? Ainsi:

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

Pourquoi devons-nous choisir ?

Je pense que nous devrions utiliser un schéma JSON comme source unique de vérité pour la structure de la configuration. -Et puis générez du code TypeScript/Java/Whatever à partir de ce schéma.

Mais JSON Schema est un peu difficile à lire pour les humains, donc pendant que nous discutons du schéma dans un problème GitHub dans cucumber/common je suggérais TypeScript pour faciliter la discussion.

Tu vois ce que je veux dire?

Les schémas JSON sont encore quelque peu lisibles pour les humains IMO

Pas pour moi :-) Trop verbeux.

Comment cela fonctionnerait-il pour un journaliste qui n'a pas besoin d'un nom de fichier de sortie ?

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

(un seul formateur peut utiliser le flux stdout)

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

jfstephe picture jfstephe  ·  4Commentaires

hdorgeval picture hdorgeval  ·  3Commentaires

edwinwright picture edwinwright  ·  3Commentaires

jan-molak picture jan-molak  ·  4Commentaires

igniteram picture igniteram  ·  7Commentaires