Conecta archivos .feature de Gherkin con pruebas automatizadas de Playwright, ejecútalas en CI, publica informes de éxito/fallo y bloquea despliegues, para que ningún código se envíe a menos que cada requisito pase.
La automatización del navegador ejecuta las pruebas
5
Se ejecuta el pipeline de GitHub Actions
En cada PR y push a main
6
Publica informe de éxito/fallo
Por escenario, por requisito
7
Permite o bloquea el despliegue
Bloqueo deploy job needs: [test]
Cada etapa es trazable. Un escenario fallido indica el requisito que cubre, la historia de usuario a la que pertenece y el comportamiento exacto que se rompió.
Los requisitos empiezan como GitHub Issues. Cada issue recibe un ID de requisito en el título y criterios de aceptación escritos como escenarios Gherkin.
Título del GitHub Issue: "REQ-101: Validación del formulario de perfil"
Etiquetas:requirement, priority: high
Como arquitecto cloud
Quiero que el formulario de perfil valide mis entradas antes del envío
Para recibir feedback inmediato y no enviar datos incompletos
Criterios de aceptación (Gherkin)
Los campos obligatorios muestran errores cuando están vacíos
El formato de email se valida
La URL de LinkedIn debe apuntar a linkedin.com
Los años de experiencia deben estar entre 0 y 50
La fecha de fin del trabajo debe ser posterior a la de inicio
Cada escenario se vincula con su requisito mediante etiquetas @REQ-xxx. Cucumber recopila estas etiquetas para los informes.
# features/profile-form/04-validation.feature@REQ-101Feature: Profile Form Validation As a Cloud Architect I want the form to validate my inputs So that I get immediate feedback on errors Background: Given the architect is logged in And they navigate to "/profile/create" @REQ-101 @critical Scenario: Required fields show errors when empty Given the architect is on step 1 "Personal Info" And they have not filled in any fields When they click "Next" Then they should see "Full name is required" And they should see "Email is required" And the form should not advance to step 2 @REQ-101 Scenario: Email format is validated Given the architect is on step 1 "Personal Info" When they type "not-an-email" in the "Email" field And they click "Next" Then they should see "Please enter a valid email address" @REQ-101 Scenario: LinkedIn URL must point to linkedin.com Given the architect is on step 1 "Personal Info" When they type "https://twitter.com/someone" in the "LinkedIn URL" field And they click "Next" Then they should see "Must be a valid LinkedIn profile URL" @REQ-102 Scenario: Years of experience must be between 0 and 50 Given the architect is on step 2 "Experience" When they type "-3" in the "Years of Experience" field And they click "Next" Then they should see "Must be between 0 and 50 years" @REQ-102 Scenario: Job end date must be after start date Given the architect is on step 3 "Job History" When they set start date to "2025-06-01" And they set end date to "2024-01-01" And they click "Next" Then they should see "End date must be after start date"
# features/profile-form/08-file-uploads.feature@REQ-201Feature: File Upload Protection As a Cloud Architect I want the form to reject invalid files So that only safe, correct images are uploaded @REQ-201 @critical Scenario: Profile photo rejects oversized file Given the architect is on step 5 "Uploads" When they select a 15MB file for "Profile Photo" Then they should see "File must be under 5MB" @REQ-201 @critical Scenario: Profile photo rejects non-image files Given the architect is on step 5 "Uploads" When they select a .exe file for "Profile Photo" Then they should see "Only JPEG, PNG, and WebP files are accepted" @REQ-201 Scenario: Maximum file count enforced Given the architect has uploaded 10 architecture diagrams When they try to add another file Then they should see "Maximum 10 files allowed"
# features/profile-form/03-multi-step.feature@REQ-301Feature: Multi-Step Navigation As a Cloud Architect I want to navigate between form steps So that I can fill out the form at my own pace @REQ-301 @critical Scenario: Completing step 1 unlocks step 2 Given the architect fills all required fields on step 1 When they click "Next" Then step 2 "Experience" should be active And step 1 should show a checkmark @REQ-301 Scenario: Navigating back preserves data Given the architect is on step 3 "Job History" And they have entered "Acme Corp" as company name When they click "Back" And they click "Next" Then the company name field should still contain "Acme Corp" @REQ-301 Scenario: Cannot skip ahead past invalid steps Given the architect is on step 1 "Personal Info" And the "Full Name" field is empty When they click step 3 in the progress indicator Then step 1 should remain active
Configura la cadena de herramientas que conecta los archivos .feature con la automatización del navegador.
npm install -D @playwright/test playwright @cucumber/cucumber @badeball/cypress-cucumber-preprocessor# Usamos @cucumber/cucumber como parser de Gherkin, no Cypress# Para Cucumber nativo en Playwright, usa playwright-bdd:npm install -D playwright-bdd
playwright-bdd es el puente recomendado: analiza archivos .feature y genera archivos de prueba de Playwright automáticamente.
Las step definitions son el pegamento. Cada Given/When/Then del archivo .feature coincide con una step definition que controla el navegador.
// features/step-definitions/navigation-steps.tsimport { createBdd } from "playwright-bdd";import { expect } from "@playwright/test";const { Given, When, Then } = createBdd();// ── Pasos de Background ───────────────────────────────────Given("the architect is logged in", async ({ page }) => { // Establece cookie de auth o usa storageState await page.goto("/api/test/login");});Given("they navigate to {string}", async ({ page }, url: string) => { await page.goto(url);});// ── Navegación entre pasos ────────────────────────────────────Given( "the architect is on step {int} {string}", async ({ page }, step: number, _name: string) => { await page.goto("/profile/create"); for (let i = 1; i < step; i++) { await fillStepWithValidData(page, i); await page.getByTestId("btn-next").click(); } await expect( page.getByTestId(`step-${step}`) ).toHaveAttribute("data-status", "current"); });Given( "the architect fills all required fields on step {int}", async ({ page }, step: number) => { await fillStepWithValidData(page, step); });When("they click {string}", async ({ page }, buttonText: string) => { await page.getByRole("button", { name: buttonText }).click();});When( "they click step {int} in the progress indicator", async ({ page }, step: number) => { await page.getByTestId(`step-${step}`).click(); });Then( "step {int} {string} should be active", async ({ page }, step: number, _name: string) => { await expect( page.getByTestId(`step-${step}`) ).toHaveAttribute("data-status", "current"); });Then("step {int} should show a checkmark", async ({ page }, step: number) => { await expect( page.getByTestId(`step-${step}`) ).toHaveAttribute("data-status", "completed");});Then("step {int} should remain active", async ({ page }, step: number) => { await expect( page.getByTestId(`step-${step}`) ).toHaveAttribute("data-status", "current");});// ── Helper ─────────────────────────────────────────────async function fillStepWithValidData(page: import("@playwright/test").Page, step: number) { switch (step) { case 1: await page.getByTestId("field-fullName").fill("Jane Doe"); await page.getByTestId("field-email").fill("jane@example.com"); break; case 2: await page.getByTestId("field-yearsOfExperience").fill("8"); await page.getByTestId("field-currentRole").fill("Principal Architect"); await page.getByTestId("cert-aws-solutions-architect-professional").click(); break; case 3: await page.getByTestId("job-0-company").fill("Acme Corp"); await page.getByTestId("job-0-role").fill("Cloud Architect"); await page.getByTestId("job-0-start").fill("2020-01-01"); await page.getByTestId("job-0-current").click(); break; case 4: await page.getByTestId("platform-aws").click(); await page.getByTestId("specialty-serverless").click(); break; case 5: // Las subidas son opcionales -- omitir break; }}
// features/step-definitions/validation-steps.tsimport { createBdd } from "playwright-bdd";import { expect } from "@playwright/test";const { Given, When, Then } = createBdd();When( "they type {string} in the {string} field", async ({ page }, value: string, fieldLabel: string) => { const field = page.getByLabel(fieldLabel, { exact: false }); await field.clear(); await field.fill(value); });When( "they set start date to {string}", async ({ page }, date: string) => { await page.getByTestId("job-0-start").fill(date); });When( "they set end date to {string}", async ({ page }, date: string) => { await page.getByTestId("job-0-end").fill(date); });Given( "they have not filled in any fields", async () => { // No-op -- los campos están vacíos por defecto });Given( "the {string} field is empty", async () => { // No-op -- el campo está vacío por defecto });Then( "they should see {string}", async ({ page }, errorText: string) => { await expect(page.getByText(errorText)).toBeVisible(); });Then( "the form should not advance to step {int}", async ({ page }, step: number) => { await expect( page.getByTestId(`step-${step}`) ).not.toHaveAttribute("data-status", "current"); });
// features/step-definitions/data-persistence-steps.tsimport { createBdd } from "playwright-bdd";import { expect } from "@playwright/test";const { Given, Then } = createBdd();Given( "they have entered {string} as company name", async ({ page }, company: string) => { await page.getByTestId("job-0-company").fill(company); });Then( "the company name field should still contain {string}", async ({ page }, expected: string) => { await expect(page.getByTestId("job-0-company")).toHaveValue(expected); });
playwright-bdd lee tus archivos .feature y las step definitions, y luego genera archivos de prueba estándar de Playwright. Nunca escribes pruebas de Playwright manualmente: salen de Gherkin.
# Genera pruebas de Playwright a partir de archivos .featurenpx bddgen# Ejecuta las pruebas generadasnpx playwright test# Ejecuta solo escenarios etiquetados con @criticalnpx playwright test --grep "@critical"# Ejecuta solo escenarios de REQ-101npx playwright test --grep "@REQ-101"
Los archivos de prueba generados tienen este aspecto (no los edites):
// .features-gen/profile-form/04-validation.feature.spec.ts (auto-generado)import { test } from "playwright-bdd";test.describe("Profile Form Validation @REQ-101", () => { test.beforeEach(async ({ Given, page }) => { await Given("the architect is logged in"); await Given('they navigate to "/profile/create"'); }); test("Required fields show errors when empty @REQ-101 @critical", async ({ Given, When, Then }) => { await Given('the architect is on step 1 "Personal Info"'); await Given("they have not filled in any fields"); await When('they click "Next"'); await Then('they should see "El nombre completo es obligatorio"'); await Then('they should see "El email es obligatorio"'); await Then("the form should not advance to step 2"); }); // ... más escenarios auto-generados desde el archivo .feature});
Las etiquetas @REQ-xxx en tus archivos .feature habilitan el seguimiento a nivel de requisito. El pipeline las analiza desde los resultados de pruebas y las reporta.
# Todos los escenarios críticosnpx playwright test --grep "@critical"# Todos los escenarios de REQ-101npx playwright test --grep "@REQ-101"# Omitir trabajo en progresonpx playwright test --grep-invert "@wip"# Solo smoke tests (comprobación rápida de bloqueo)npx playwright test --grep "@smoke"
deploy: needs: [test] # ← Will not run if test job fails if: github.ref == 'refs/heads/main' environment: production # ← Optional: require manual approval too
Si quieres permitir despliegues cuando fallan pruebas no críticas, añade un job separado solo para críticos:
jobs: test-all: name: Run All Gherkin Tests runs-on: ubuntu-latest steps: # ... same setup ... - name: Run all tests run: npx playwright test continue-on-error: true # Don't fail the job # ... publish reports ... test-critical: name: Run Critical Tests (Deploy Gate) runs-on: ubuntu-latest steps: # ... same setup ... - name: Run critical tests only run: npx playwright test --grep "@critical" # This job WILL fail if any @critical scenario fails deploy: needs: [test-critical] # ← Only gated by critical tests # test-all results are informational only
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
# 1. Escribe archivos .feature con etiquetas @REQ-xxx# features/profile-form/04-validation.feature# 2. Escribe step definitions# features/step-definitions/validation-steps.ts# 3. Genera pruebas de Playwright a partir de los .featurenpx bddgen# 4. Ejecuta localmentenpx playwright test# 5. Ejecuta un requisito específiconpx playwright test --grep "@REQ-101"# 6. Ejecuta solo críticos (bloqueo de despliegue)npx playwright test --grep "@critical"# 7. Ver informe HTMLnpx playwright show-report
Cuándo usarlo: Cuando ya tienes archivos .feature y step definitions funcionando. Este documento muestra cómo integrarlos en CI/CD para que apliquen los requisitos en cada PR.
npx bddgen debe ejecutarse antes de npx playwright test -- los archivos de prueba generados en .features-gen/ no existen hasta que ejecutes el generador. Añádelo como paso de CI antes del paso de pruebas.
.features-gen/ debe estar en .gitignore -- son archivos generados. Hacer commit provoca conflictos de merge y pruebas obsoletas.
Los tipos de parámetro en las step definitions importan -- {int} coincide con números, {string} con cadenas entre comillas. Tipos de parámetro incorrectos provocan errores de "step not found" difíciles de depurar.
El filtrado por etiquetas usa --grep, no --tags -- Playwright usa --grep para filtrar, no la sintaxis --tags de Cucumber. @REQ-101 se convierte en --grep "@REQ-101".
continue-on-error: true vs if: always() -- usa continue-on-error en el paso de pruebas para permitir la publicación de informes. Usa if: always() en los pasos de subida de artefactos. No los confundas o perderás fallos.
La ruta del reporter JSON debe coincidir con el script de análisis -- la ruta de results.json en playwright.config.ts debe coincidir con lo que lee el script de GitHub Actions. Ambos usan por defecto test-results/results.json.
La protección de entorno es independiente del needs del job -- needs: [test] bloquea el job de despliegue. Las reglas de protección de entorno añaden aprobación manual. Usa ambos para despliegues a producción.