Cucumber Cheat Sheet
Complete reference guide for Cucumber with interactive examples and live playground links
Click on any section to jump directly to it
Gherkin Syntax (Cucumber)
Core Keywords
Essential Gherkin keywords for writing BDD scenarios in Cucumber
Feature: Feature description
As a [role]
I want [capability]
So that [benefit]
Scenario: Scenario description
Given [precondition]
When [action]
Then [expected result]
Background:
Given [common precondition]
Scenario Outline: Outline description
Given [precondition]
When [action] with <parameter>
Then [expected result]
Examples:
| parameter |
| value1 |
| value2 |
Keyword Meanings
Understanding what each Gherkin keyword represents in Cucumber
Feature: - Describes a major functionality/feature
Scenario: - Describes a specific behavior/test case
Given: - Sets up the initial context/state
When: - Describes the action/event that occurs
Then: - Describes the expected outcome/result
And: - Continues the previous step type
But: - Continues the previous step type (negative)
Background: - Common setup for multiple scenarios
Examples: - Data table for Scenario Outline
Writing Patterns
User Story Format
Standard format for writing user stories in Cucumber/Gherkin
Feature: User Authentication
As a registered user
I want to log into the system
So that I can access my account
Scenario: Successful login with valid credentials
Given I am on the login page
And I have valid credentials
When I enter my username and password
And I click the "Login" button
Then I should be redirected to the dashboard
And I should see a welcome message
Data-Driven Scenarios
Using Scenario Outline for testing with multiple data sets
Scenario Outline: Login with different user types
Given I am on the login page
When I enter "<username>" and "<password>"
And I click the "Login" button
Then I should see "<expected_message>"
Examples:
| username | password | expected_message |
| admin | admin123 | Welcome Admin |
| user | user123 | Welcome User |
| guest | guest123 | Access Denied |
Step Definitions
TypeScript Step Definitions
Implementing step definitions in TypeScript with Cucumber and Playwright
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { chromium, Browser, Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
let browser: Browser;
let page: Page;
let loginPage: LoginPage;
Before(async function() {
browser = await chromium.launch({ headless: false });
page = await browser.newPage();
this.page = page;
});
After(async function() {
await browser?.close();
});
Given('I am on the login page', async function() {
await this.page.goto('https://example.com/login');
loginPage = new LoginPage(this.page);
});
When('I enter {string} and {string}', async function(username: string, password: string) {
await loginPage.enterUsername(username);
await loginPage.enterPassword(password);
});
When('I click the Login button', async function() {
await loginPage.clickLoginButton();
});
Then('I should be redirected to the dashboard', async function() {
const currentUrl = await this.page.url();
expect(currentUrl).toContain('/dashboard');
});
Then('I should see {string}', async function(message: string) {
const pageContent = await this.page.textContent('body');
expect(pageContent).toContain(message);
});
World Context
Using World context to share data between steps
import { setWorldConstructor, World } from '@cucumber/cucumber';
import { Page, Browser } from '@playwright/test';
export class CustomWorld extends World {
public page!: Page;
public browser!: Browser;
public testData: any = {};
async initBrowser() {
this.browser = await chromium.launch();
this.page = await this.browser.newPage();
}
async cleanup() {
await this.browser?.close();
}
}
setWorldConstructor(CustomWorld);
// Using World context in steps
Given('I am logged in as {string}', async function(username: string) {
this.testData.username = username;
await this.page.goto('/login');
// Login logic here
});
When('I perform an action', async function() {
// Access shared data
const username = this.testData.username;
// Action logic here
});
Page Object Model
TypeScript Page Object
Creating page objects in TypeScript with Playwright
import { Page, Locator } from '@playwright/test';
export class LoginPage {
private page: Page;
private usernameField: Locator;
private passwordField: Locator;
private loginButton: Locator;
private errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameField = page.locator('#username');
this.passwordField = page.locator('#password');
this.loginButton = page.locator('#login-button');
this.errorMessage = page.locator('.error-message');
}
async enterUsername(username: string) {
await this.usernameField.clear();
await this.usernameField.fill(username);
}
async enterPassword(password: string) {
await this.passwordField.clear();
await this.passwordField.fill(password);
}
async clickLoginButton() {
await this.loginButton.click();
}
async getErrorMessage(): Promise<string> {
return await this.errorMessage.textContent() || '';
}
async isErrorMessageDisplayed(): Promise<boolean> {
return await this.errorMessage.isVisible();
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
}
Base Page Object
Creating a base page object for common functionality
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async navigateTo(path: string) {
await this.page.goto(path);
await this.page.waitForLoadState('networkidle');
}
async waitForElement(locator: Locator, timeout = 5000) {
await locator.waitFor({ timeout });
}
async takeScreenshot(name: string) {
await this.page.screenshot({
path: `screenshots/${name}-${Date.now()}.png`,
fullPage: true
});
}
async getPageTitle(): Promise<string> {
return await this.page.title();
}
}
export class DashboardPage extends BasePage {
private welcomeMessage: Locator;
private logoutButton: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.locator('.welcome-message');
this.logoutButton = page.locator('#logout-button');
}
async getWelcomeMessage(): Promise<string> {
return await this.welcomeMessage.textContent() || '';
}
async logout() {
await this.logoutButton.click();
}
}
Configuration & Setup
Package.json Setup
Complete Node.js setup for Cucumber with Playwright
{
"name": "cucumber-playwright-tests",
"version": "1.0.0",
"description": "BDD tests with Cucumber and Playwright",
"scripts": {
"test": "cucumber-js",
"test:parallel": "cucumber-js --parallel 4",
"test:smoke": "cucumber-js --tags @smoke",
"test:regression": "cucumber-js --tags @regression",
"report": "node generate-report.js"
},
"devDependencies": {
"@cucumber/cucumber": "^9.0.0",
"@playwright/test": "^1.35.0",
"@types/node": "^18.0.0",
"ts-node": "^10.9.0",
"typescript": "^4.9.0",
"cucumber-html-reporter": "^7.0.0",
"cucumber-pretty": "^6.0.0"
}
}
Cucumber Configuration
Cucumber.js configuration for TypeScript and Playwright
// cucumber.js
module.exports = {
default: {
requireModule: ['ts-node/register'],
require: [
'src/step-definitions/**/*.ts',
'src/hooks/**/*.ts',
'src/support/**/*.ts'
],
format: [
'progress-bar',
'html:reports/cucumber-report.html',
'json:reports/cucumber-report.json',
'cucumber-pretty'
],
formatOptions: {
snippetInterface: 'async-await',
theme: {
'feature keyword': ['blue', 'bold'],
'scenario keyword': ['blue', 'bold'],
'step keyword': ['blue', 'bold']
}
},
publishQuiet: true,
parallel: 2
},
smoke: {
tags: '@smoke',
format: ['progress-bar', 'html:reports/smoke-report.html']
},
regression: {
tags: '@regression',
format: ['progress-bar', 'html:reports/regression-report.html']
}
};
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
expect: { timeout: 5000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }]
],
use: {
baseURL: 'https://example.com',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Best Practices
Writing Guidelines
Best practices for writing effective Cucumber/Gherkin scenarios
✅ DO:
- Write in business language, not technical terms
- Keep scenarios focused and atomic
- Use descriptive step names
- Make scenarios independent
- Use Background for common setup
- Use data tables for complex data
- Tag scenarios appropriately
❌ DON'T:
- Include implementation details
- Write UI-specific steps ("click blue button")
- Make scenarios dependent on each other
- Use technical jargon
- Write overly complex scenarios
- Hardcode test data in scenarios
Example of good vs bad:
✅ "When I click the Login button"
❌ "When I click the blue button with ID 'login-btn'"
✅ "When I enter valid credentials"
❌ "When I enter 'admin' and 'password123'"
Step Definition Patterns
Best practices for implementing step definitions
// Use async/await for all async operations
When('I click the Login button', async function() {
await this.page.click('#login-button');
});
// Use parameters for flexibility
When('I enter {string} and {string}', async function(username: string, password: string) {
await this.page.fill('#username', username);
await this.page.fill('#password', password);
});
// Use data tables for complex data
When('I fill in the user details:', async function(dataTable) {
const userData = dataTable.rowsHash();
await this.page.fill('#name', userData.name);
await this.page.fill('#email', userData.email);
await this.page.selectOption('#role', userData.role);
});
// Use World context for shared state
Given('I am logged in as {string}', async function(username: string) {
this.currentUser = username;
// Login logic here
});
// Handle errors gracefully
Then('I should see an error message', async function() {
await expect(this.page.locator('.error-message')).toBeVisible();
const errorText = await this.page.locator('.error-message').textContent();
expect(errorText).toMatch(/invalid|error|failed/i);
});
CI/CD Integration
GitHub Actions
Setting up Cucumber tests in GitHub Actions with Playwright
name: Cucumber BDD Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18]
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Cucumber tests
run: npm run test
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: cucumber-reports-node-18
path: |
reports/
test-results/
screenshots/
retention-days: 30
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: Cucumber Test Results
path: reports/cucumber-report.json
reporter: cucumber-json
Docker Integration
Running Cucumber tests in Docker containers
# Dockerfile
FROM mcr.microsoft.com/playwright:v1.35.0-focal
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY src/ ./src/
COPY cucumber.js ./
# Install Playwright browsers
RUN npx playwright install --with-deps
# Run tests
CMD ["npm", "run", "test"]
# docker-compose.yml
version: '3.8'
services:
cucumber-tests:
build: .
environment:
- NODE_ENV=test
- BASE_URL=https://example.com
volumes:
- ./reports:/app/reports
- ./screenshots:/app/screenshots
command: npm run test:parallel
# Run with Docker
# docker build -t cucumber-playwright .
# docker run -v $(pwd)/reports:/app/reports cucumber-playwright
Advanced Features
Tags and Organization
Using tags to organize and filter scenarios
@smoke @login
Feature: User Authentication
As a user
I want to authenticate
So that I can access the system
@smoke
Scenario: Basic login
Given I am on the login page
When I enter valid credentials
Then I should be logged in
@regression @edge-case
Scenario: Login with special characters
Given I am on the login page
When I enter username with special chars
Then I should see validation error
@api @integration
Scenario: API authentication
Given I have valid API credentials
When I make an authentication request
Then I should receive a valid token
# Running tagged scenarios:
# npm run test -- --tags @smoke
# npm run test -- --tags "not @slow"
# npm run test -- --tags "@smoke and @login"
# npm run test -- --tags "@api or @integration"
Hooks and Setup
Using hooks for setup and teardown operations
import { Before, After, BeforeAll, AfterAll } from '@cucumber/cucumber';
import { chromium, Browser } from '@playwright/test';
let browser: Browser;
BeforeAll(async function() {
browser = await chromium.launch();
});
Before(async function() {
this.page = await browser.newPage();
await this.page.setViewportSize({ width: 1280, height: 720 });
});
After(async function() {
if (this.page) {
await this.page.close();
}
});
AfterAll(async function() {
await browser?.close();
});
// Tagged hooks
Before('@database', async function() {
// Setup test database
await setupTestDatabase();
});
After('@cleanup', async function() {
// Clean up test data
await cleanupTestData();
});
// Screenshot on failure
After(async function(scenario) {
if (scenario.result?.status === 'FAILED') {
const screenshot = await this.page.screenshot({
path: `screenshots/failure-${scenario.pickle.name}-${Date.now()}.png`,
fullPage: true
});
this.attach(screenshot, 'image/png');
}
});
Cucumber - Interactive Developer Reference
Hover over code blocks to copy or run in live playground