BDD with Cucumber — End-to-End Tutorial (2026)
Ship your first Cucumber BDD suite in a day. Feature files, step definitions, hooks, tags, and CI wiring for Java, JS, and Python. Includes when BDD hurts and how to avoid the Gherkin trap.

Last updated 2026-07-20 · 14 min read · By Avinash K
Cucumber done right closes the gap between product, dev, and QA. Cucumber done wrong is Selenium tests with extra YAML. This tutorial ships you a working BDD suite in Java, JS, or Python and calls out the four mistakes that make teams abandon Cucumber after six months.
Key takeaways
- Working Cucumber setup in 15 minutes across 3 languages.
- Gherkin best practices from real product-QA collaboration.
- Tags, hooks, and parallel execution.
- When BDD adds cost without value — how to spot it early.
1. When BDD is worth it (and when it isn't)
Worth it when product/BA/QA collaborate on scenarios before code exists. Not worth it when QA writes both the feature file and the step definitions with no product involvement — you added indirection for nothing.
2. Your first feature file
Feature: Checkout coupon
As a shopper I want to apply coupons so I can save money
Scenario: Expired coupon is rejected
Given I have a cart with one item
And the coupon "EXPIRED2025" has expired
When I apply the coupon on checkout
Then I see the message "This coupon has expired"
And the cart total is unchanged3. Step definitions in JavaScript (Cucumber-JS + Playwright)
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
Given('I have a cart with one item', async function () {
await this.api.seedCart(this.user, [{ sku: 'BOOK-1', qty: 1 }]);
});
When('I apply the coupon on checkout', async function () {
await this.page.goto('/checkout');
await this.page.getByLabel('Coupon').fill(this.coupon);
await this.page.getByRole('button', { name: 'Apply' }).click();
});
Then('I see the message {string}', async function (msg: string) {
await expect(this.page.getByRole('alert')).toHaveText(msg);
});4. Step definitions in Java (Cucumber-JVM + Selenium)
@Given("I have a cart with one item")
public void iHaveACart() { api.seedCart(user, "BOOK-1", 1); }
@When("I apply the coupon on checkout")
public void iApplyCoupon() {
driver.get(BASE + "/checkout");
driver.findElement(By.id("coupon")).sendKeys(coupon);
driver.findElement(By.id("apply")).click();
}
@Then("I see the message {string}")
public void iSeeMessage(String msg) {
assertEquals(msg, driver.findElement(By.cssSelector("[role=alert]")).getText());
}6. Four Cucumber traps to avoid
- Imperative Gherkin ("I click the button") — write declarative business intent instead.
- Step explosion — dedupe and parameterize.
- Shared world state — leaks between scenarios. Reset in hooks.
- No product participation — if only QA reads .feature files, delete Cucumber.
Pair with the test automation complete guide and see our SpecFlow tutorial for C# for the .NET flavor.