by thetestingacademy
Behavior-driven development with Cucumber, Gherkin, and step definitions
npx @qaskills/cli add bdd-cucumberAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
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
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
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
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 |
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/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/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();
}
});
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")
);
}
}
}
// 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);
// 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}`);
}
});
# 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
@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"
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'],
},
};
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();
}
}
- name: Install QA Skills
run: npx @qaskills/cli add bdd-cucumber10 of 29 agents supported