BDD and Gherkin: A Shared Language
The previous page gave TDD an honest reckoning: it disciplines design and pins behaviour, but the tests it produces are written by developers, for developers, in a programming language. That is fine when the person who knows what “correct” means is the developer. It falls apart when the person who knows the rules — a product owner, a domain expert, a compliance officer, a tester — cannot read the test that claims to encode their rule.
Behavior-driven development (BDD) is the answer to that gap. It is not a new kind of testing so much as TDD pointed at a different audience: instead of starting from a failing unit test, you start from a failing example of a behaviour, written in language a non-developer can read, argue with, and sign off on. This page teaches the notation (Gherkin’s Given/When/Then), how those plain-language sentences become executable tests, the “living specification” idea that makes the whole thing worth the effort — and the very real ways BDD goes wrong.
BDD is TDD reframed around behaviour
Section titled “BDD is TDD reframed around behaviour”TDD’s loop is red-green-refactor: write a failing test, make it pass, clean up. BDD keeps that loop exactly, but changes two things about the test.
First, it changes the vocabulary. A TDD test is often named after a method — test_apply_discount — and asserts on return values. A BDD example is named after a behaviour a stakeholder cares about — “a returning customer gets free shipping over £50” — and is phrased as an observable outcome, not an internal call.
Second, and more importantly, it changes who writes it with you. The whole reason BDD exists is a communication failure that no amount of unit testing fixes: the developer builds what the ticket said, the ticket was a lossy summary of what the business meant, and nobody discovers the gap until the feature ships wrong. BDD attacks that gap by making the specification a conversation artefact — something the developer, the tester, and the business person produce together, before the code exists, in words all three understand.
TDD BDD ─── ─── dev writes a failing test dev + tester + business agree in code, for a method on an example, in plain language │ │ ▼ ▼ red → green → refactor red → green → refactor (same loop underneath) (same loop underneath) │ │ ▼ ▼ audience: developers audience: everyone, incl. non-devsThe engine underneath is identical. What changes is that the specification is now readable by the people who own the requirements. This is sometimes called “specification by example”: rather than describing a rule abstractly (“premium members get priority support”), you pin it with concrete examples (“Given a premium member, when they open a ticket, then it is tagged priority”) that are precise enough to execute and plain enough to read aloud in a planning meeting.
Gherkin: Given / When / Then
Section titled “Gherkin: Given / When / Then”Gherkin is the small, deliberately English-like language most BDD tools use to write those examples. Its entire grammar fits on a postcard, and that is the point — it has to be writable by someone who has never programmed.
A Gherkin file (a .feature file) has two levels. A Feature names a capability and gives context. Under it, one or more Scenarios each describe one concrete example of the feature, using three keywords:
- Given — the starting state, the context you set up (“the world is like this”).
- When — the single action or event under test (“this happens”).
- Then — the expected observable outcome (“we can see this”).
And / But continue whichever step came before, so you are never forced to cram everything into one line.
Here is a complete, concrete feature for a shopping-cart discount rule:
Feature: Free shipping for larger orders As a shopper I want free shipping once my order is large enough So that I am rewarded for buying more
Scenario: Order over the threshold ships free Given my cart contains items totalling £60.00 When I proceed to checkout Then the shipping cost is £0.00 And the order total is £60.00
Scenario: Order under the threshold pays shipping Given my cart contains items totalling £40.00 When I proceed to checkout Then the shipping cost is £4.99 And the order total is £44.99
Scenario: Order exactly at the threshold ships free Given my cart contains items totalling £50.00 When I proceed to checkout Then the shipping cost is £0.00Read those three scenarios and you know the rule exactly — including the corner that plain prose almost always fudges. “Free shipping over £50” is ambiguous about £50.00 itself; the third scenario nails it down as inclusive. This is the same edge-case discipline from Boundary Value Analysis, except now the boundary is written in a sentence a product owner can approve. The Given/When/Then shape forces you to name the state, the trigger, and the result separately, which is exactly the Arrange–Act–Assert structure wearing business clothes.
The Rule of One When
Section titled “The Rule of One When”A well-formed scenario has exactly one When. Multiple Whens are the classic beginner smell: they mean you have jammed two behaviours into one example, and when it fails you will not know which action broke. If you feel the urge to write a second When, that is usually a second scenario asking to be born. Given and Then may repeat freely (state has many facts; outcomes have many observable parts), but the event under test should be singular.
From plain language to executable test: step definitions
Section titled “From plain language to executable test: step definitions”A .feature file is just text — English with three keywords. It does nothing on its own. The magic of BDD is the layer that connects each plain-language step to real code: the step definitions.
A step definition is a function, tagged with a pattern that matches a Gherkin line. When the BDD runner (Cucumber for Ruby/Java/JS, SpecFlow/Reqnroll for .NET, behave for Python, godog for Go, and others) reads the feature, for each step it finds the definition whose pattern matches and runs it. The Gherkin sentence is the specification; the step definition is the glue that drives the real system to satisfy it.
.feature line step definition (code) ───────────── ─────────────────────── Given my cart contains items ─────► @given("my cart contains items totalling £60.00 totalling £{amount}") def step(ctx, amount): ctx.cart = Cart(total=amount)
When I proceed to checkout ─────► @when("I proceed to checkout") def step(ctx): ctx.result = checkout(ctx.cart)
Then the shipping cost is £0.00 ─────► @then("the shipping cost is £{amount}") def step(ctx, amount): assert ctx.result.shipping == amountA Python-flavoured (behave) implementation of those three steps:
from behave import given, when, then
@given('my cart contains items totalling £{amount:f}')def step_cart_total(context, amount): context.cart = Cart(subtotal=amount) # Arrange
@when('I proceed to checkout')def step_checkout(context): context.result = checkout(context.cart) # Act — the single event
@then('the shipping cost is £{amount:f}')def step_shipping(context, amount): assert context.result.shipping == amount # Assert
@then('the order total is £{amount:f}')def step_total(context, amount): assert context.result.total == amountThree things are worth noticing. The {amount:f} parameter capture means one step definition serves every scenario that mentions a total — the £60, £40, and £50 lines all reuse the same glue. The shared context object carries state from Given to When to Then, so the steps compose into one flowing example. And the assertions live only in Then steps — a When that asserts is a design mistake, because the “did it work?” question belongs to the outcome, not the action.
Now the loop is pure red-green-refactor. Write the feature, run it: every step is either undefined (the runner even prints a skeleton for you to fill in) or failing — that is your red. Implement checkout until all three scenarios pass — green. Refactor with the scenarios as your net. Same discipline as TDD; the red is just phrased in English.
The living specification
Section titled “The living specification”Here is the idea that makes BDD more than fancy test naming. Once your acceptance criteria are written as executable Gherkin, the specification and the test suite are the same document.
Think about what that collapses. In a traditional project you have (1) a requirements doc describing what the system should do, and (2) a separate test suite checking whether it does. These two drift apart the instant either changes — the requirements doc becomes the confident lie we warned about in Tests as Living Documentation. BDD refuses to let them drift by making them one artefact: the .feature files are the requirements, and they are executed on every CI run. A requirement that no longer matches the code cannot survive, because the build goes red.
Traditional: BDD: requirements.docx (prose, rots) features/*.feature ▲ drift ▼ │ executed every CI run tests/*.py (code, runs) ▼ two documents, silently disagree ONE document that cannot lieThis is what “living specification” means. The features are readable enough to hand to a stakeholder as documentation and executable enough to serve as the acceptance test. Tools like Cucumber’s HTML reports or Pickles/living-doc generators render the passing features into a browsable spec, so a business person can open a page, read the current, guaranteed-true behaviour of the system in plain English, and see a green tick beside each rule they signed off on. The documentation cannot be stale, for the same reason a unit test cannot be stale: a stale one fails the build.
The costs BDD does not advertise
Section titled “The costs BDD does not advertise”BDD’s brochure is seductive, and teams routinely adopt it, feel the friction, and quietly abandon it. Know the costs before you buy.
Step-definition maintenance is a second codebase. Every Gherkin sentence needs a matching step definition, and that glue layer is real code you must write, refactor, and debug. A large suite accumulates hundreds of steps; keep them too specific and they explode in number, keep them too generic and they become an unreadable mini-language of their own. When a Then fails, you now debug across two layers — the feature text and the glue — instead of one. The indirection that buys readability also buys a longer path from red bar to root cause.
Brittle wording breaks tests for non-reasons. Because steps match on text, a scenario and its step definition are coupled by an English string. Someone rewrites When I proceed to checkout as When I check out to read better; the step no longer matches; the scenario fails though the system is perfectly correct. Multiply that across a shared step library and you get tests that break on prose edits — a maintenance tax that pure-code tests, refactored by a compiler-aware IDE, simply do not pay.
BDD-as-ceremony is the cardinal sin. The entire justification for Gherkin’s overhead — the parser, the glue, the string-matching brittleness — is that a non-developer actually reads and writes the features. If the only people who ever open a .feature file are the same developers who wrote the step definitions, you have paid every cost of BDD and received none of its benefit. You would have been faster writing plain unit tests. This is the most common way BDD fails: adopted as a mandate, the three-way conversation skipped, the features authored solo by a developer translating their own unit tests into stilted English for an audience of nobody. Gherkin without a business reader is pure ceremony.
The honest rule: use Gherkin only for the behaviours a non-developer needs to read. A handful of acceptance scenarios that a product owner reviews is BDD working as intended. Wrapping your entire test pyramid — every arithmetic helper and null check — in Given/When/Then is the anti-pattern, and it is why so many teams associate BDD with slow, fragile, write-only test suites.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because the most expensive software defects are not coding errors but shared-understanding errors — the developer built exactly what the ticket said, and the ticket was wrong. BDD exists to make the specification something the business, the tester, and the developer author together, in words all three can read.
- What problem does it solve? It closes the gap between “what the stakeholder meant” and “what the test checks” by making them the same sentence. Given/When/Then examples are precise enough to execute yet plain enough to approve in a planning meeting, so requirements and acceptance tests stop being two documents that drift.
- What are the trade-offs? You gain a business-readable, always-current living specification; you pay for it with a whole extra layer — step definitions — that is real code to maintain, plus brittleness where scenarios are coupled to their step text by an English string, plus indirection that lengthens debugging.
- When should I avoid it? Avoid Gherkin for behaviours no non-developer will ever read — internal algorithms, arithmetic helpers, plumbing. There, plain unit tests are faster, less brittle, and refactor-safe. Reserve BDD for the acceptance-level rules a stakeholder actually reviews.
- What breaks if I remove it? For a team where devs and domain owners already share a language, little — good unit and integration tests cover the behaviour. For a team divided by a requirements-to-code translation gap, removing BDD removes the one artefact that forced that conversation to happen before the wrong thing was built.
Check your understanding
Section titled “Check your understanding”- In one or two sentences, explain how BDD relates to TDD: what stays the same, and what two things change?
- Write a Gherkin scenario (Feature line plus one Given/When/Then scenario) for the rule “a user is locked out after 3 failed login attempts.” Include the boundary case you think matters most.
- A
.featurefile is plain text and does nothing by itself. What connects a Gherkin line to running code, and what does parameter capture (e.g.{amount:f}) buy you across many scenarios? - Explain the “living specification” idea. Why can a Gherkin feature not silently become stale the way a requirements document can?
- Name three concrete costs of BDD, and state the one condition under which the whole approach degrades into pure ceremony that would have been better served by plain unit tests.
Show answers
- BDD keeps TDD’s red-green-refactor loop unchanged — you still write a failing check, make it pass, then clean up. What changes is (a) the vocabulary: examples are named and phrased around observable behaviours a stakeholder cares about rather than around methods and return values; and (b) the authorship and audience: the specification is written by developer, tester, and business person together, in language all three can read, instead of by a developer for developers.
- For example:
The boundary that matters is the transition from the 2nd to the 3rd failure (locked at 3, not before) — the exact off-by-one place “after 3 attempts” is ambiguous, just like the £50 threshold. A second scenario checking that the 2nd failure does not lock is the natural pair.Feature: Account lockout after repeated failuresScenario: Third consecutive failure locks the accountGiven a user with 2 previous failed login attemptsWhen they submit an incorrect passwordThen the account is lockedAnd a lockout notification is sent
- Step definitions connect them: each is a function tagged with a pattern that matches a Gherkin line, and the BDD runner (Cucumber, SpecFlow/Reqnroll,
behave,godog, …) executes the matching definition for each step. Parameter capture like{amount:f}lets a single step definition serve every scenario that mentions a total (£60, £40, £50 all reuse one glue function), so you write and maintain far fewer step definitions than you have scenario lines. - The living specification is the fact that, in BDD, the requirements document and the acceptance-test suite are the same artefact: the
.featurefiles are readable enough to hand a stakeholder as documentation and executable enough to run on every CI build. It cannot go stale for the same reason a unit test cannot — if the feature no longer matches the code, the build goes red, so a requirement that drifted out of sync fails loudly instead of quietly lying, unlike a prose requirements doc that nothing re-verifies. - Costs: (1) step-definition maintenance — the glue layer is a second real codebase to write, refactor, and debug across two layers; (2) brittle wording — scenarios are coupled to step definitions by English strings, so a harmless prose edit can break a passing test; (3) indirection that lengthens the path from a red bar to the root cause. It degrades into ceremony when no non-developer ever reads or writes the features — if only the developers who wrote the step definitions ever open the
.featurefiles, you have paid every cost of BDD and gained none of its benefit, and plain unit tests would have been faster and less fragile.