Qaskills BDD/Cucumber Patterns
Behavior-Driven Development skill using Cucumber, covering feature files, step definitions, Gherkin best practices, data tables, scenario outlines, and hooks.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/bdd-cucumber" ~/.claude/skills/pramoddutta-qaskills-bdd-cucumber-patterns && rm -rf "$T"
manifest:
seed-skills/bdd-cucumber/SKILL.mdsource content
BDD/Cucumber Patterns Skill
You are an expert QA engineer specializing in Behavior-Driven Development (BDD) with Cucumber. When the user asks you to write, review, or improve Cucumber feature files and step definitions, follow these detailed instructions.
Core Principles
- Business language -- Feature files must use domain language that non-technical stakeholders understand.
- Declarative over imperative -- Describe what the user does, not how the UI works.
- Single scenario, single behavior -- Each scenario tests exactly one business rule.
- Reusable step definitions -- Steps should be generic enough to reuse across features.
- Living documentation -- Feature files are the single source of truth for behavior.
Project Structure (TypeScript)
features/ auth/ login.feature registration.feature password-reset.feature products/ product-listing.feature product-search.feature checkout/ cart.feature payment.feature step-definitions/ auth.steps.ts products.steps.ts checkout.steps.ts common.steps.ts support/ world.ts hooks.ts custom-parameter-types.ts pages/ login.page.ts products.page.ts cucumber.js tsconfig.json
Project Structure (Java)
src/ test/ java/com/example/ steps/ AuthSteps.java ProductSteps.java CommonSteps.java pages/ LoginPage.java ProductsPage.java hooks/ Hooks.java runners/ TestRunner.java resources/ features/ auth/ login.feature registration.feature products/ product-listing.feature
Writing Feature Files
Good Feature File
Feature: User Login As a registered user I want to log into the application So that I can access my personalized dashboard Background: Given the login page is displayed @smoke @auth Scenario: Successful login with valid credentials When I log in with valid credentials Then I should see the dashboard And I should see a welcome message @auth @negative Scenario: Login fails with incorrect password When I log in with an incorrect password Then I should see an error message "Invalid email or password" And I should remain on the login page @auth @negative Scenario: Login fails with non-existent email When I log in with a non-registered email Then I should see an error message "Invalid email or password" @auth @security Scenario: Account locks after multiple failed attempts When I attempt to log in 5 times with incorrect passwords Then my account should be temporarily locked And I should see a message about account lockout
Scenario Outline (Data-Driven)
Feature: Form Validation As a user I want to see clear validation messages So that I can correct my input @validation Scenario Outline: Email validation Given I am on the registration page When I enter "<email>" in the email field And I submit the form Then I should see the validation message "<message>" Examples: | email | message | | | Email is required | | not-an-email | Please enter a valid email | | @missing.com | Please enter a valid email | | valid@example.com | | @validation Scenario Outline: Password strength validation Given I am on the registration page When I enter "<password>" in the password field And I move to the next field Then the password strength indicator should show "<strength>" Examples: | password | strength | | abc | weak | | abcdef12 | medium | | SecurePass123! | strong |
Data Tables
Scenario: Create multiple users Given the following users exist: | email | name | role | | admin@example.com | Admin User | admin | | user1@example.com | User One | user | | user2@example.com | User Two | viewer | When I navigate to the user management page Then I should see 3 users in the list Scenario: Verify user profile details Given I am logged in as "admin@example.com" When I view my profile Then my profile should contain: | Field | Value | | Name | Admin User | | Email | admin@example.com | | Role | Administrator | Scenario: Add items to cart When I add the following items to my cart: | product | quantity | price | | Widget A | 2 | 29.99 | | Widget B | 1 | 49.99 | Then my cart total should be "$109.97"
Step Definitions (TypeScript)
// step-definitions/auth.steps.ts import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from '@playwright/test'; import { CustomWorld } from '../support/world'; Given('the login page is displayed', async function (this: CustomWorld) { await this.page.goto('/login'); await expect(this.page.getByRole('heading', { name: 'Sign In' })).toBeVisible(); }); When('I log in with valid credentials', async function (this: CustomWorld) { await this.loginPage.login('user@example.com', 'SecurePass123!'); }); When('I log in with an incorrect password', async function (this: CustomWorld) { await this.loginPage.login('user@example.com', 'wrongpassword'); }); When('I log in with a non-registered email', async function (this: CustomWorld) { await this.loginPage.login('nonexistent@example.com', 'SomePass123!'); }); Then('I should see the dashboard', async function (this: CustomWorld) { await expect(this.page).toHaveURL(/\/dashboard/); }); Then('I should see a welcome message', async function (this: CustomWorld) { await expect(this.page.getByText(/welcome/i)).toBeVisible(); }); Then('I should see an error message {string}', async function (this: CustomWorld, message: string) { await expect(this.page.getByRole('alert')).toHaveText(message); }); Then('I should remain on the login page', async function (this: CustomWorld) { await expect(this.page).toHaveURL(/\/login/); }); When('I attempt to log in {int} times with incorrect passwords', async function ( this: CustomWorld, attempts: number ) { for (let i = 0; i < attempts; i++) { await this.loginPage.login('user@example.com', `wrong${i}`); } }); Then('my account should be temporarily locked', async function (this: CustomWorld) { await expect(this.page.getByText(/locked/i)).toBeVisible(); });
Step Definitions with Data Tables
// step-definitions/common.steps.ts import { Given, When, Then, DataTable } from '@cucumber/cucumber'; import { CustomWorld } from '../support/world'; Given('the following users exist:', async function (this: CustomWorld, dataTable: DataTable) { const users = dataTable.hashes(); for (const user of users) { await this.apiClient.post('/api/users', { email: user.email, name: user.name, role: user.role, password: 'DefaultPass123!', }); } }); Then('my profile should contain:', async function (this: CustomWorld, dataTable: DataTable) { const expectedData = dataTable.rowsHash(); for (const [field, value] of Object.entries(expectedData)) { const element = this.page.getByLabel(field); await expect(element).toHaveValue(value as string); } }); When('I add the following items to my cart:', async function (this: CustomWorld, dataTable: DataTable) { const items = dataTable.hashes(); for (const item of items) { await this.page.getByText(item.product).click(); await this.page.getByLabel('Quantity').fill(item.quantity); await this.page.getByRole('button', { name: 'Add to Cart' }).click(); } });
Step Definitions (Java)
package com.example.steps; import io.cucumber.java.en.*; import io.cucumber.datatable.DataTable; import static org.assertj.core.api.Assertions.*; import java.util.List; import java.util.Map; public class AuthSteps { private final LoginPage loginPage; private final DashboardPage dashboardPage; public AuthSteps() { this.loginPage = new LoginPage(DriverFactory.getDriver()); this.dashboardPage = new DashboardPage(DriverFactory.getDriver()); } @Given("the login page is displayed") public void theLoginPageIsDisplayed() { loginPage.navigate(); assertThat(loginPage.isDisplayed()).isTrue(); } @When("I log in with valid credentials") public void iLogInWithValidCredentials() { loginPage.loginAs("user@example.com", "SecurePass123!"); } @When("I log in with an incorrect password") public void iLogInWithIncorrectPassword() { loginPage.loginAs("user@example.com", "wrongpassword"); } @Then("I should see the dashboard") public void iShouldSeeTheDashboard() { assertThat(dashboardPage.isDisplayed()).isTrue(); } @Then("I should see an error message {string}") public void iShouldSeeAnErrorMessage(String expectedMessage) { assertThat(loginPage.getErrorMessage()).isEqualTo(expectedMessage); } @Given("the following users exist:") public void theFollowingUsersExist(DataTable dataTable) { List<Map<String, String>> users = dataTable.asMaps(); for (Map<String, String> user : users) { apiClient.createUser( user.get("email"), user.get("name"), user.get("role") ); } } }
World and Hooks
Custom World (TypeScript)
// support/world.ts import { World, IWorldOptions, setWorldConstructor } from '@cucumber/cucumber'; import { Browser, Page, chromium } from '@playwright/test'; import { LoginPage } from '../pages/login.page'; export class CustomWorld extends World { browser!: Browser; page!: Page; loginPage!: LoginPage; apiClient: any; testData: Map<string, any> = new Map(); constructor(options: IWorldOptions) { super(options); } async init() { this.browser = await chromium.launch({ headless: true }); const context = await this.browser.newContext(); this.page = await context.newPage(); this.loginPage = new LoginPage(this.page); } async cleanup() { await this.page?.close(); await this.browser?.close(); } } setWorldConstructor(CustomWorld);
Hooks
// support/hooks.ts import { Before, After, BeforeAll, AfterAll, BeforeStep, AfterStep, Status } from '@cucumber/cucumber'; import { CustomWorld } from './world'; Before(async function (this: CustomWorld) { await this.init(); }); After(async function (this: CustomWorld, scenario) { if (scenario.result?.status === Status.FAILED) { const screenshot = await this.page.screenshot(); this.attach(screenshot, 'image/png'); console.log(`Scenario failed: ${scenario.pickle.name}`); } await this.cleanup(); }); Before({ tags: '@auth' }, async function (this: CustomWorld) { // Set up authentication state for auth-tagged scenarios await this.apiClient?.login('admin@example.com', 'AdminPass123!'); }); After({ tags: '@cleanup' }, async function (this: CustomWorld) { // Clean up test data created during the scenario for (const [key, value] of this.testData.entries()) { await this.apiClient?.delete(`/api/${key}/${value}`); } });
Gherkin Best Practices
Declarative vs Imperative
# BAD -- Imperative (too detailed, UI-coupled) Scenario: Login Given I navigate to "https://example.com/login" When I click on the email field And I type "user@example.com" in the email field And I click on the password field And I type "SecurePass123!" in the password field And I click the "Sign In" button Then I should be redirected to "/dashboard" And the h1 element should contain "Welcome" # GOOD -- Declarative (business-focused) Scenario: Successful login Given I am on the login page When I log in with valid credentials Then I should see my dashboard And I should see a welcome message
Tags for Organization
@auth @regression Feature: User Authentication @smoke @critical Scenario: Successful login ... @negative Scenario: Login with invalid password ... @security @slow Scenario: Account lockout after failed attempts ...
Run selective tests:
# Run smoke tests only npx cucumber-js --tags "@smoke" # Run auth tests that are not slow npx cucumber-js --tags "@auth and not @slow" # Run critical or smoke tests npx cucumber-js --tags "@critical or @smoke"
Configuration
cucumber.js (TypeScript)
module.exports = { default: { require: ['step-definitions/**/*.ts', 'support/**/*.ts'], requireModule: ['ts-node/register'], format: [ 'progress', 'json:reports/cucumber-report.json', 'html:reports/cucumber-report.html', ], paths: ['features/**/*.feature'], publishQuiet: true, }, smoke: { require: ['step-definitions/**/*.ts', 'support/**/*.ts'], requireModule: ['ts-node/register'], tags: '@smoke', paths: ['features/**/*.feature'], }, };
TestRunner (Java)
package com.example.runners; import io.cucumber.testng.AbstractTestNGCucumberTests; import io.cucumber.testng.CucumberOptions; import org.testng.annotations.DataProvider; @CucumberOptions( features = "src/test/resources/features", glue = {"com.example.steps", "com.example.hooks"}, plugin = { "pretty", "html:target/cucumber-reports/cucumber.html", "json:target/cucumber-reports/cucumber.json", "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm" }, tags = "@smoke or @regression", monochrome = true, dryRun = false ) public class TestRunner extends AbstractTestNGCucumberTests { @Override @DataProvider(parallel = true) public Object[][] scenarios() { return super.scenarios(); } }
Best Practices
- Write features before code -- BDD means features drive development.
- Use domain language -- Avoid technical terms in feature files.
- Keep scenarios short -- 3-8 steps per scenario is ideal.
- One behavior per scenario -- Do not test multiple things in one scenario.
- Use Background for shared Given steps -- Avoid repeating setup across scenarios.
- Use Scenario Outlines for data-driven tests -- Avoid duplicating similar scenarios.
- Tag strategically -- Use tags for test selection, not as test metadata.
- Keep step definitions thin -- Delegate to page objects or service classes.
- Use a consistent voice -- First person ("I") or third person ("the user"), not both.
- Review features with stakeholders -- Features are communication tools, not just tests.
Anti-Patterns to Avoid
- UI details in features -- "I click the blue button" is implementation detail.
- Technical jargon -- "The API returns a 200 status" is not business language.
- Too many steps -- More than 10 steps per scenario means it is testing too much.
- Coupled scenarios -- Each scenario must be independent.
- Scenario as a test script -- Features describe behavior, not test procedures.
- Unused step definitions -- Remove dead code; it confuses maintenance.
- Regex-heavy step definitions -- Use Cucumber expressions instead of complex regex.
- Hardcoded data in steps -- Use parameters and data tables for flexibility.
- No tags -- Without tags, you cannot run subsets of tests.
- Treating features as afterthoughts -- Writing features after code defeats the purpose of BDD.