Given-When-Then
The Gherkin vocabulary for describing behaviour, how it maps onto arrange-act-assert, and when a shared specification language is worth its cost.
2 min read · updated 19 September 2026
Given some context, When something happens, Then something should be observably true.
Feature: Refunds
Scenario: A damaged item is refunded in full
Given a paid order for 2 copies of "Field Notes" at £12.00
When the customer requests a refund because the item was damaged
Then the order status is "refunded"
And £24.00 is credited to the original payment method
And 2 copies are returned to stockIt is arrange-act-assert with different words, and the different words are the whole point: these are aimed at people who will never open the test file.
#The rule that makes it work
Given-when-then describes behaviour, not interaction. The moment a scenario says click, navigate or enter, it has stopped being a specification and become a script — and a script written in plain English is strictly worse than a script written in code.
Compare:
# Bad: a UI script with English syntax
Given I am on "/login"
When I enter "[email protected]" in the "#email" field
And I enter "hunter2" in the "#password" field
And I click "#submit"
Then I should see ".dashboard-header"
# Good: a statement about behaviour
Given Alice has a verified account
When she signs in with her correct password
Then she sees her dashboardThe first breaks when the selector changes and tells a reader nothing they could not get from the code. The second survives a redesign, survives moving from web to mobile, and is a sentence a product owner can confirm or deny.
#Steps map to code, not to clicks
// TypeScript, Cucumber. The step is a sentence; the implementation is
// ordinary automation code that can change freely underneath it.
import { Given, When, Then } from '@cucumber/cucumber';
Given('Alice has a verified account', async function () {
this.alice = await createVerifiedCustomer({ email: '[email protected]' });
});
When('she signs in with her correct password', async function () {
await this.page.goto('/signin');
await this.page.getByLabel('Email').fill(this.alice.email);
await this.page.getByLabel('Password').fill(this.alice.password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
});
Then('she sees her dashboard', async function () {
await expect(this.page.getByRole('heading', { name: /your dashboard/i })).toBeVisible();
});// C#, SpecFlow/Reqnroll — same idea in .NET
[Given(@"a paid order for (\d+) copies of ""(.*)"" at £(.*)")]
public void GivenAPaidOrder(int quantity, string title, decimal price) =>
_order = OrderBuilder.Paid(title, quantity, (int)(price * 100));
[When(@"the customer requests a refund because the item was damaged")]
public void WhenARefundIsRequested() => _refund = _order.Refund(RefundReason.Damaged);
[Then(@"the order status is ""(.*)""")]
public void ThenTheStatusIs(string expected) =>
Assert.Equal(expected, _order.Status.ToString().ToLowerInvariant());#Without a framework at all
Most of the value is available with no Gherkin layer whatsoever. The vocabulary works perfectly well inside a normal test:
def test_a_damaged_item_is_refunded_in_full():
# Given a paid order for 2 copies at £12.00
order = paid_order(title="Field Notes", quantity=2, unit_cents=1_200)
# When the customer requests a refund because the item was damaged
refund = order.refund(reason=RefundReason.DAMAGED)
# Then the full amount is credited and stock is restored
assert order.status == "refunded"
assert refund.amount_cents == 2_400
assert order.stock_to_restore == 2This is the right default. Adopt feature files when there is a person outside engineering who will genuinely read and challenge them — see BDD for the honest assessment of how often that is true.
#Where scenarios go wrong
Too many givens. Four setup lines means the scenario is describing
incidental context. Move it to a Background, or to a single given that
names the situation: Given a customer with an active subscription.
Multiple whens. Two actions means two behaviours. The exception is a genuine sequence — when she submits, and then the payment provider declines — where the second event is not the user's.
Thens that assert implementation. Then a row is inserted into the orders table is not something the reader of a specification can confirm
and it couples the scenario to a schema.
Scenario explosion. Twelve scenarios differing by one value is a
Scenario Outline with an examples table, and if the table has thirty rows
the whole thing belongs in a parameterised unit test instead.
#The honest cost
A Gherkin layer adds an indirection: a sentence, a regex, a step definition, a helper. That costs real time to write and real time to navigate. It pays for itself when the sentences are read and argued about by people who could not otherwise participate.
If the feature files are written by developers, read by developers, and maintained by developers, the layer is pure overhead — and the vocabulary, used in test names and comments, gives you nearly all the benefit for free. See test naming.
Common questions
- Do I need Cucumber to use given-when-then?
- No. Given-when-then is a vocabulary; Cucumber is one tool for executing it from plain-text files. Most teams get the value of the vocabulary from well-named tests and never need the feature-file layer.
- What is the difference between given-when-then and arrange-act-assert?
- They describe the same three phases. AAA is aimed at the developer reading the code; given-when-then is aimed at a shared conversation with people who do not read code. Using GWT words in a codebase nobody outside engineering reads adds ceremony without the benefit.
- How many 'and' steps is too many?
- If a scenario has four givens, the setup is incidental detail that belongs in a background or a helper. A scenario with three whens is two scenarios. Long scenarios are the main reason feature files stop being read by the people they were written for.
Runnable samples for this page
last test results ↗- Gherkin
cucumber/features
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Behaviour-Driven DevelopmentBDD as a conversation practice rather than a tool choice — what the three amigos session produces, when Cucumber earns its place, and how it fails.
- Arrange-Act-AssertThe three-part shape every readable test has, why the act step should be one line, and the smells that show up when a test will not fit the pattern.
- Naming and Structuring TestsTest names that say what broke without opening the file, the naming conventions worth adopting, and how to organise a suite so people can find things.
- SDET vs Automation Tester vs Manual QAThree genuinely different jobs that are routinely advertised as seniority levels of one — what each actually does, and what goes wrong when the distinction is lost.
- Unit Testing without MocksThe sociable approach — real collaborators, state-based assertions, and tests that survive refactoring. What it buys, where it breaks down, and how to keep it fast.
- RSpecRuby's specification-style framework — describe and context blocks, let and subject, matchers, and the readability trade it makes.