Conecte arquivos Gherkin .feature a testes automatizados do Playwright, execute-os em CI, publique relatórios de aprovação/falha e controle implantações -- para que nenhum código seja enviado a menos que cada requisito passe.
Requisitos começam como GitHub Issues. Cada issue recebe um ID de requisito no título e critérios de aceitação escritos como cenários Gherkin.
Título da GitHub Issue: "REQ-101: Validação do Formulário de Perfil"
Labels:requirement, priority: high
Como um Arquiteto de Nuvem
Eu quero que o formulário de perfil valide minhas entradas antes da submissão
Para que eu receba feedback imediato e não envie dados incompletos
Critérios de Aceitação (Gherkin)
Campos obrigatórios mostram erros quando vazios
Formato de e-mail é validado
URL do LinkedIn deve apontar para linkedin.com
Anos de experiência devem ser de 0 a 50
Data de término do trabalho deve ser posterior à data de início
Cada cenário se vincula ao seu requisito via tags @REQ-xxx. O Cucumber coleta essas tags para relatórios.
# 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
Configure a toolchain que conecta arquivos .feature à automação do navegador.
npm install -D @playwright/test playwright @cucumber/cucumber @badeball/cypress-cucumber-preprocessor# Usamos @cucumber/cucumber para o parser Gherkin, não Cypress# Para Cucumber nativo do Playwright, use playwright-bdd:npm install -D playwright-bdd
playwright-bdd é a ponte recomendada -- ela analisa arquivos .feature e gera arquivos de teste Playwright automaticamente.
As definições de passo são a cola. Cada Given/When/Then no arquivo feature corresponde a uma definição de passo que controla o navegador.
// features/step-definitions/navigation-steps.tsimport { createBdd } from "playwright-bdd";import { expect } from "@playwright/test";const { Given, When, Then } = createBdd();// ── Background steps ───────────────────────────────────Given("the architect is logged in", async ({ page }) => { // Set auth cookie or use storageState await page.goto("/api/test/login");});Given("they navigate to {string}", async ({ page }, url: string) => { await page.goto(url);});// ── Step navigation ────────────────────────────────────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: // Uploads are optional -- skip 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 -- fields are empty by default });Given( "the {string} field is empty", async () => { // No-op -- field is empty by default });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 lê seus arquivos .feature e definições de passo, depois gera arquivos de teste Playwright padrão. Você nunca escreve testes Playwright manualmente -- eles vêm do Gherkin.
# Gerar testes Playwright a partir de arquivos .featurenpx bddgen# Executar os testes geradosnpx playwright test# Executar apenas cenários com tag @criticalnpx playwright test --grep "@critical"# Executar apenas cenários REQ-101npx playwright test --grep "@REQ-101"
Os arquivos de teste gerados se parecem com isto (você não edita estes):
// .features-gen/profile-form/04-validation.feature.spec.ts (auto-generated)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 "Full name is required"'); await Then('they should see "Email is required"'); await Then("the form should not advance to step 2"); }); // ... more scenarios auto-generated from the .feature file});
O workflow de CI é executado em cada PR e push para main. Ele gera testes a partir de features, os executa, publica relatórios e controla a implantação.
# .github/workflows/gherkin-deploy.ymlname: Gherkin Test & Deploy Pipelineon: push: branches: [main] pull_request: branches: [main]permissions: contents: read checks: write pull-requests: writejobs: # ── Stage 1: Generate and run Cucumber/Playwright tests ── test: name: Run Gherkin Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Generate tests from .feature files run: npx bddgen - name: Build application run: npm run build - name: Run Gherkin Playwright tests run: npx playwright test env: CI: true # ── Publish test results ── - name: Upload Playwright report uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 - name: Upload test results JSON uses: actions/upload-artifact@v4 if: always() with: name: test-results path: test-results/ retention-days: 30 # ── Post results to PR as comment ── - name: Parse test results and post PR comment if: always() && github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const results = JSON.parse( fs.readFileSync('test-results/results.json', 'utf8') ); // Count pass/fail per @REQ tag const reqMap = new Map(); for (const suite of results.suites ?? []) { for (const spec of suite.specs ?? []) { const tags = spec.title.match(/@REQ-\d+/g) ?? []; const passed = spec.tests.every(t => t.status === 'expected'); for (const tag of tags) { if (!reqMap.has(tag)) reqMap.set(tag, { pass: 0, fail: 0, scenarios: [] }); const entry = reqMap.get(tag); passed ? entry.pass++ : entry.fail++; entry.scenarios.push({ title: spec.title, passed }); } } } // Build markdown table let body = '## Gherkin Test Results\n\n'; body += '| Requirement | Passed | Failed | Status |\n'; body += '|-------------|--------|--------|--------|\n'; for (const [req, data] of [...reqMap.entries()].sort()) { const status = data.fail === 0 ? '✅ Pass' : '❌ Fail'; body += `| ${req} | ${data.pass} | ${data.fail} | ${status} |\n`; } const totalPass = [...reqMap.values()].reduce((s, d) => s + d.pass, 0); const totalFail = [...reqMap.values()].reduce((s, d) => s + d.fail, 0); body += `\n**Total: ${totalPass} passed, ${totalFail} failed**\n`; body += '\n📊 [Full Report](../actions/runs/' + context.runId + ')\n'; // Post or update PR comment const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number: context.issue.number, }); const existing = comments.find(c => c.body?.includes('## Gherkin Test Results') ); if (existing) { await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body, }); } else { await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body, }); } # ── Post JUnit results as GitHub Check ── - name: Publish JUnit results uses: mikepenz/action-junit-report@v4 if: always() with: report_paths: test-results/junit.xml check_name: Gherkin BDD Results detailed_summary: true include_passed: true # ── Stage 2: Deploy (gated by test job) ── deploy: name: Deploy to Production needs: [test] runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' && github.event_name == 'push' environment: name: production url: https://your-app.vercel.app steps: - uses: actions/checkout@v4 - name: Deploy to Vercel run: npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }} env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
As tags @REQ-xxx em seus arquivos .feature permitem o rastreamento em nível de requisito. O pipeline analisa essas tags a partir dos resultados dos testes e as reporta.
# Todos os cenários críticosnpx playwright test --grep "@critical"# Todos os cenários para REQ-101npx playwright test --grep "@REQ-101"# Ignorar trabalho em progressonpx playwright test --grep-invert "@wip"# Apenas testes smoke (verificação rápida de gate)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
Se você quiser permitir implantações quando testes não críticos falharem, adicione um job separado somente crítico:
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
Cartão de receita de referência rápida -- pronto para copiar e colar.
# 1. Escreva arquivos .feature com tags @REQ-xxx# features/profile-form/04-validation.feature# 2. Escreva definições de passo# features/step-definitions/validation-steps.ts# 3. Gere testes Playwright a partir de featuresnpx bddgen# 4. Execute localmentenpx playwright test# 5. Execute requisito específiconpx playwright test --grep "@REQ-101"# 6. Execute somente críticos (gate de implantação)npx playwright test --grep "@critical"# 7. Veja o relatório HTMLnpx playwright show-report
Quando usar isso: Depois de ter arquivos .feature e definições de passo funcionando. Este documento mostra como integrá-los ao CI/CD para que eles apliquem os requisitos em cada PR.
npx bddgen deve ser executado antes de npx playwright test -- os arquivos de teste gerados em .features-gen/ não existem até que você execute o gerador. Adicione-o como uma etapa de CI antes da etapa de teste.
.features-gen/ deve ser ignorado pelo git -- estes são arquivos gerados. Commitá-los causa conflitos de merge e testes desatualizados.
Tipos de parâmetros de definição de passo importam -- {int} corresponde a números, {string} corresponde a strings entre aspas. Tipos de parâmetros incompatíveis causam erros de "passo não encontrado" que são difíceis de depurar.
Filtragem de tags usa --grep e não --tags -- Playwright usa --grep para filtragem, não a sintaxe --tags do Cucumber. @REQ-101 se torna --grep "@REQ-101".
continue-on-error: true vs if: always() -- use continue-on-error na etapa de teste para permitir a publicação de relatórios. Use if: always() nas etapas de upload de artefatos. Não os confunda ou você perderá falhas.
O caminho do reporter JSON deve corresponder ao script de análise -- o caminho results.json em playwright.config.ts deve corresponder ao que o script do GitHub Actions lê. Ambos têm como padrão test-results/results.json.
Proteção de ambiente é separada do needs do job -- needs: [test] bloqueia o job de implantação. Regras de proteção de ambiente adicionam aprovação manual. Use ambos para implantações de produção.