Cucumber-js: Propuesta: API programática para ejecutar cucumber-js

Creado en 28 jun. 2021  ·  29Comentarios  ·  Fuente: cucumber/cucumber-js

Problema

Actualmente no tenemos una buena forma de ejecutar cucumber-js mediante programación. La necesidad es desde dos ángulos:

  • Probando cucumber-js - para el CCK y nuestras pruebas de aceptación en el proyecto
  • Prueba de fragmentos y formateadores personalizados
  • Proyectos que usan pepino-js como parte de un marco (por ejemplo, Serenity, Stryker)

Como es

Lo que tiende a suceder en este momento es que se crea una nueva instancia de Cli con una entrada argv encadenada . Obviamente, es muy sencillo y tampoco está en la API pública .

A veces (posiblemente debido a la fragilidad percibida de lo anterior), los marcos solo se basarán en la CLI de pepino-js, pero tendrán dificultades para encontrar formas de integrarse y tener sus propias opciones.

La clase Runtime es actualmente parte de la API pública, pero no es útil en estos contextos, dependiendo de los pickles y el código de soporte que proporcionará la persona que llama.

Propuesta

Dos componentes del proyecto:

runCucumber

Nueva función asíncrona que ejecuta una ejecución de prueba en proceso. Responsabilidades:

  • Acepta un objeto de opciones con una interfaz agradable
  • Hacer el "trabajo" de resolver pickles, cargar código de soporte, ejecutar casos de prueba, orquestar formateadores
  • Devuelve una promesa que se resuelve en un resultado

Esto sería parte de la API pública y alentaríamos a los mantenedores del marco a usarlo cuando "envuelvan" cucumber-js. También lo usaríamos para nuestras propias pruebas.

En la medida de lo posible, evitaría la interacción directa con process , aceptando en su lugar opciones normalizadas e interfaces de transmisión para la salida, y dejando que la persona que llama decida cómo salir en función del resultado o un error no controlado.

Además, Runtime debería salir de la API pública, ya que en realidad es algo interno.

CLI

Efectivamente un "cliente" de runCucumber . Responsabilidades:

  • Opciones agregadas de varias fuentes (argv, env vars, archivos de configuración) (ver comentario )
  • Llame a runCucumber con las opciones resueltas
  • Salga según corresponda según los resultados

Esto continuaría sin estar en la API pública. Además, solo usaría funciones / interfaces que están en la API pública, de modo que podríamos dividirlo fácilmente en su propio paquete en algún momento, como es un patrón común ahora con proyectos como Jest .

Este desacoplamiento también allana el camino para algunas características nuevas e interesantes de la CLI sin que se filtren en el interior, por ejemplo:

  • --gui por el pepino-electrón
  • --interactive para reposiciones rápidas y específicas al realizar TDD

etc

También expondríamos funciones (consumibles por CLI y por otros) para:

  • Obteniendo las opciones
  • Manejo de i18nKeywords y i18nLanguages

Escala de tiempo

Apuntaremos a esto en la próxima versión 8.0.0. Estoy listo para empezar con esto hoy.

breaking change enhancement

Comentario más útil

¿Cómo funcionaría esto para un reportero que no necesita un nombre de archivo de salida?

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

(solo un formateador puede usar la secuencia stdout)

Todos 29 comentarios

Paginación para comentarios iniciales: @aslakhellesoy @charlierudolph @ aurelien- reeves @mattwynne @nicojs @ jan-molak

¡Amo esta propuesta! Ya tenemos una API como esta en runCucumber de fake-cucumber

@davidjgoss - ¡suena genial!

Para su referencia, así es como Serenity / JS invoca a Cucumber en este momento: CucumberCLIAdapter
Y aquí está la lógica para convertir los parámetros de configuración a argv - CucumberOptions .

Ser capaz de proporcionar un objeto de opciones en lugar de argv sería mucho más agradable 👍🏻

¡Me encanta!

Mientras especificamos esa nueva API pública, también podemos considerar lo que sucedió recientemente con el problema # 1489 y pensar en proporcionar API públicas para tener más y mejor interacción con los filtros y las características resultantes bajo prueba.

Tener una API pública es mejor que nada, ¡así que adelante 👍!

Preferiblemente, también tendría una API para cargar perfiles usando las mismas reglas que cucumber-js , por lo que puedo imitar el comportamiento exacto de una llamada normal de pepino-js.

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

StrykerJS también dependerá en gran medida de la API custom_formatters y los eventos publicados por eventBroadcaster . ¿Podríamos agregarlos también a la API pública? Ver: https://github.com/stryker-mutator/stryker-js/blob/03b1f20ed933d3a50b52022cfe363c606c2b16c5/packages/cucumber-runner/src/stryker-formatter.ts#L45 -L69

Preferiblemente, también tendría una API para cargar perfiles usando las mismas reglas que cucumber-js, por lo que puedo imitar el comportamiento exacto de una llamada normal de cucumber-js.

Este es un buen punto. Los perfiles (al menos en su forma actual) están fundamentalmente acoplados a la CLI, por lo que se siente bien mantenerlos en ese lado del límite, pero aún podríamos exponer una función para cargarlos y generar un objeto de opciones parciales.

Al especificar esa nueva API pública, también podemos considerar lo que sucedió recientemente con el problema # 1489 y pensar en proporcionar API públicas para tener más y mejor interacción con los filtros y las características resultantes bajo prueba.

Creo que podríamos incluir una opción para proporcionar un filtro de salmuera personalizado al llamar a la API (además de los nombres, etiquetas, etc. que impulsan el filtrado integrado).

La sintaxis actual de los perfiles es muy de línea de comandos-y.

Me gustaría ❤️❤️❤️ poder especificar perfiles en un formato más genérico como JSON, JavaScript, YAML o variables de entorno. En JSON podría verse así:

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

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

O incluso con variables de entorno (por ejemplo cargadas con una herramienta como 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

De hecho, la biblioteca de configuración hace exactamente esto. Nunca terminamos integrándolo en Cucumber-JVM porque otras cosas se interpusieron en el camino, pero tal vez podríamos intentarlo con una implementación de JavaScript.

@aslakhellesoy está de acuerdo en que sería genial! Intentaré conseguir un POC para esta propuesta para que tengamos algo un poco más concreto de qué hablar y nos encantaría hacer los perfiles correctos como parte de ella (4.5 años y contando con el # 751 😄).

Refs. N.º 1004

Este es un buen punto. Los perfiles (al menos en su forma actual) están fundamentalmente acoplados a la CLI, por lo que se siente bien mantenerlos en ese lado del límite, pero aún podríamos exponer una función para cargarlos y generar un objeto de opciones parciales.

Sí, sería increíble y muy apreciado desde el punto de vista de los creadores de complementos.

Me gustaría ❤️❤️❤️ poder especificar perfiles en un formato más genérico como JSON, JavaScript, YAML o variables de entorno. En JSON podría verse así:

¡Eso suena genial! Y es también exactamente la razón por la que agradecería una API para cargarlos de la misma manera que lo hace cucumberJS. Cargar un solo archivo cucumber.js es trivial. Replicar un algoritmo de carga de archivos de configuración, incluida la precedencia, el formato de archivo, etc. Y mantenerlo es algo completamente distinto 😅.

P: ¿Podría ejecutar runCucumber dos veces seguidas _ sin borrar la caché requerida _? Esto es importante para el caso de uso de pruebas de mutación.

Queremos cargar el entorno y ejecutar las pruebas varias veces en rápida sucesión mientras cambiamos una variable global para cambiar el mutante activo.

En este momento, estamos usando la API privada cli y necesitamos borrar los archivos de definición de pasos de require.cache entre cada ejecución de prueba. Esto no es ideal para CommonJS y no funcionará en absoluto para esm.

Pseudo código de nuestro 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 está de acuerdo en que necesitamos esto, ha surgido algunas veces antes, por ejemplo, con personas que desean ejecutar pepino en una lambda, y también me gustaría agregar un modo interactivo que también lo necesitaría.

Lo que había esbozado hasta ahora tenía un estilo más funcional pero creo que el mismo concepto fundamental:

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

Eso funciona para nosotros 👍

También funciona para nosotros 👍🏻

Realmente creo que algo como esto sería extremadamente útil como alternativa al gran lío que tenemos hoy con las integraciones de herramientas de prueba (Jest, Cypress), por ejemplo encontré estos problemas (en orden de importancia):

  • cypress-cucumber-preprocessor no admite etiquetas en Ejemplos (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/196)
  • jest-cucumber no admite informes JSON de pepino (https://github.com/bencompton/jest-cucumber/issues/27)
  • cypress-cucumber-preprocessor genera varios informes Cucumber JSON sin soporte oficial para la agregación (https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/issues/423)
  • jest-cucumber no es tan conveniente como jest-cucumber-fusion
  • también hay cucumber-jest ...
  • Karma ya no tiene una implementación funcional (https://github.com/cucumber/cucumber-js/issues/1095)

Preferiría ver un código de pegamento mínimo entre Jest / Karma / Cypress / etc. y cucumber-js para no tener que sufrir por todas esas funciones faltantes que necesito usar.

Gran sugerencia @davidjgoss 👍

Esta separación de preocupaciones entre la interfaz de usuario de la línea de comandos y la "lógica empresarial" de analizar y ejecutar escenarios como pruebas me recuerda el patrón de arquitectura hexagonal .

En cucumber-ruby, en realidad dividimos la lógica del dominio central (o "hexágono interno") en un paquete de gemas separado, ya que lo estábamos reconstruyendo desde cero en una "sala limpia". Me doy cuenta de que ese no es el contexto aquí, pero podría valer la pena inspirarse o retroalimentar las innovaciones de este diseño en la API de Ruby. Hay un ejemplo en el archivo README de la gema cucumber-ruby-core de cómo usar esa API.

Bien, aquí tienes un primer paso a la firma de la API para el bit de "ejecución". Se basa en gran medida en el objeto IConfiguration que tenemos internamente (por lo que no debería causar demasiada refactorización en la parte inferior) pero solo un poco 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
}

Y un ejemplo de uso muy 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,
    },
  },
})

¡Comentarios bienvenidos! Tenga en cuenta que esto no cubre las cosas de carga de perfil / configuración, que sería otra función.

Creo que esto se ve bien. Pregunta: ¿Cómo puedo configurar un formateador personalizado?

@nicojs algo así como en la CLI

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

¡Es genial ver el progreso en este @davidjgoss!

No quiero ralentizar el progreso en esto, pero al mismo tiempo quiero asegurarme de que adoptemos un formato que también pueda funcionar para otras implementaciones de Cucumber.

Eventualmente, un esquema JSON, pero mientras lo discutimos, creo que los tipos de TypeScript son más fáciles de analizar para los humanos.

Sugiero que creemos una nueva edición sobre el formato propuesto en el monorepo cucumber/common e invitemos al equipo central a discutir allí.

@aslakhellesoy servirá.

¿Qué pensaría de que la API programática no esté vinculada a la estructura de opciones comunes? Como si hiciéramos un mapa de eso a las opciones runCucumber . Agrega tal vez un poco de complejidad, pero atrae debido a cosas como tener un bloque support que es parámetros para cargar o una biblioteca de código de soporte cargada previamente. Podría hacer algo similar para las características + encurtidos también. Y hay varias opciones que admitiríamos en la CLI (por ejemplo, --exit ) que no son apropiadas en la API programática.

¿Qué pensaría de que la API programática no esté vinculada a la estructura de opciones comunes?

Creo que está bien, siempre que proporcionemos una función que convierta del contenido del archivo de opciones a la estructura de datos que runCucumber quiere.

Eventualmente, un esquema JSON, pero mientras lo discutimos, creo que los tipos de TypeScript son más fáciles de analizar para los humanos.

¿Por qué tenemos que elegir? Estamos usando el esquema JSON en StrykerJS para generar mecanografiado usando json-schema-to-typescript . No estamos asignando los archivos de salida de TS al control de código fuente, sino que los generamos sobre la marcha usando un paso prebuild .

Los esquemas JSON todavía son algo legibles para humanos, en mi opinión. Ya hemos tenido relaciones públicas en el repositorio de Stryker y la gente parece saber qué hacer 🤷‍♀️

algo así como en la CLI

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

¿Cómo funcionaría esto para un reportero que no necesita un nombre de archivo de salida? Al igual que:

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

¿Por qué tenemos que elegir?

Creo que deberíamos usar un esquema JSON como una única fuente de verdad para la estructura de la configuración. -Y luego genere TypeScript / Java / Cualquier código de ese esquema.

Pero JSON Schema es un poco difícil de leer para los humanos, así que mientras discutimos el esquema en un problema de GitHub en cucumber/common , estaba sugiriendo TypeScript para facilitar la discusión.

¿Ves lo que quiero decir?

Los esquemas JSON todavía son algo legibles para humanos, en mi opinión

No para mí :-) Demasiado detallado.

¿Cómo funcionaría esto para un reportero que no necesita un nombre de archivo de salida?

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

(solo un formateador puede usar la secuencia stdout)

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

jfstephe picture jfstephe  ·  4Comentarios

jan-molak picture jan-molak  ·  4Comentarios

hdorgeval picture hdorgeval  ·  3Comentarios

kozhevnikov picture kozhevnikov  ·  6Comentarios

jechazelle picture jechazelle  ·  5Comentarios