Given, When, Then, And, But: BDD Step Rules

Most teams adopt Gherkin and immediately start writing scenarios that technically pass but communicate nothing useful. The keywords become decoration — Given slapped on anything that happens before the action, Then used for intermediate state checks, When stretched to cover multi-step flows. Cucumber-JVM 7, Behave, and SpecFlow all execute these scenarios without complaint. The parser doesn't care. Your future self, debugging a regression at 11 PM, will.

The problem isn't syntax — it's intent mapping. Given, When, and Then are not arbitrary labels; they encode a specific causal model: context, event, outcome. When that model breaks down, scenarios lose their diagnostic value. A failing Then step should tell you what the system got wrong. A failing When step should tell you what action couldn't be performed. Conflating them turns a readable specification into a cryptic test log.

By the end of this article you'll have a precise rule for each keyword, a clear test for when And and But earn their place, and a set of anti-patterns to audit in your existing suite. These rules apply whether you're running Playwright against a React SPA or Selenium 4 against a legacy Java monolith.

API Testing using Python, Behave, VS Code & GitHub Copilot

Smarter API Test Automation — Python, Behave, VS Code, AI with GitHub Copilot & CI/CD Pipelines. Complete in a Weekend!

Learn more

Given, When, Then Defined — The Causal Model Behind the Syntax

The Given-When-Then structure maps directly to the Arrange-Act-Assert pattern — but with a deliberate emphasis on business language. Given establishes preconditions: the state of the world before the scenario begins. It should be idempotent and side-effect-free from the user's perspective — seeding a database row, authenticating a session, or setting a feature flag. When describes a single, discrete event — the action a user or external system takes. One When per scenario is a strong default; two is a warning sign; three is a test that needs splitting. Then asserts observable outcomes: what the system now exposes to the outside world. It should never trigger further state changes.

And and But are continuations — they inherit the semantic role of the keyword above them. And chains steps of the same type; But introduces a contrasting condition within the same type. They exist purely for readability; step definitions in Behave, Cucumber-JVM 7, or SpecFlow bind to them identically. The practical rule: if removing And and replacing it with the parent keyword reads naturally, you're using it correctly. This Given/When/Then model is the foundation on which outside-in TDD and BDD diverge most sharply — BDD scenarios describe observable system behavior, not internal state transitions.

Applying the Keywords Precisely: Rules, Edge Cases, and Code

The clearest rule for When: it should describe something a named actor does, or something an external system sends. If you can't name the actor, the step probably belongs in Given. Here's a scenario that gets this right:

Feature: Checkout flow

  Background:
    Given the product catalog contains "Wireless Keyboard" at £49.99
    And the inventory count for "Wireless Keyboard" is 12

  Scenario: Guest checkout reduces inventory
    Given a guest user has "Wireless Keyboard" in their cart
    When the guest completes checkout with card ending "4242"
    Then the order confirmation page displays order ID
    And the inventory count for "Wireless Keyboard" is 11
    But no loyalty points are credited to any account

The Background block handles global preconditions shared across scenarios — keeping individual Given steps focused on scenario-specific context. The single When is one user action. The Then checks two observable outcomes; But adds a contrasting assertion without introducing a new keyword. In Behave, the step bindings look like this:

# steps/checkout_steps.py  (Behave)
from behave import given, when, then

@given('a guest user has "{product}" in their cart')
def step_guest_cart(context, product):
    context.cart = context.browser.add_to_cart(product, guest=True)

@when('the guest completes checkout with card ending "{last4}"')
def step_complete_checkout(context, last4):
    context.order = context.browser.checkout(card_last4=last4)

@then('the order confirmation page displays order ID')
def step_order_confirmed(context):
    assert context.order.confirmation_id is not None

@then('the inventory count for "{product}" is {count:d}')
def step_inventory_count(context, product, count):
    actual = context.inventory_api.get_count(product)
    assert actual == count, f"Expected {count}, got {actual}"

Notice that no Then step calls context.browser to navigate anywhere — it only reads state. This is the boundary that matters. Teams that put navigation inside Then steps end up with scenarios where a single failure cascades silently across assertions. If you're using Playwright with Pytest-BDD, the same rule applies; soft assertions that accumulate across BDD steps are especially dangerous when Then steps also mutate state.

The "multiple Whens" problem

A scenario with two When steps is almost always two scenarios. The exception: a multi-step wizard where each step is part of one atomic user journey and splitting would destroy readability. In that case, use And to chain them — but document the decision in a comment so the next engineer doesn't refactor it back. In Cucumber-JVM 7, there's no runtime penalty either way; the cost is entirely in comprehension and fault isolation. Run time for a well-structured suite with proper Background usage dropped from 18 minutes to 4 in one real migration by eliminating redundant Given setup duplicated across 60 scenarios — moving shared state into Background and fixture hooks instead.

Where does Background end and Given begin?

Use Background for state that every scenario in the feature file requires. Use scenario-level Given for state that varies. If more than 70% of your scenarios share a Given step verbatim, it belongs in Background. If you find yourself writing conditional logic inside a Background step definition, the feature file has grown too large and needs splitting — a structural issue covered in detail when setting up the core files every BDD project requires.

Step Keyword Mistakes Senior Engineers Still Ship

The most common mistake is using When for setup actions that have no business-visible trigger — things like "When the database is seeded with test data" or "When the feature flag is enabled." These are preconditions, not events. They belong in Given or, better, in a @before hook. The reason this happens is tooling: Cucumber-JVM, Behave, and SpecFlow all bind step definitions regardless of keyword, so the tests pass. The cost is invisible until someone reads the scenario and can't tell what the system under test actually does.

The second mistake is writing Then steps that call navigation methods or trigger side effects — clicking a "download" button inside an assertion, or calling an API endpoint to verify state by changing it. This pattern is especially common in teams migrating from Selenium 4 WebDriver scripts where the assertion and the action were in the same method. In Playwright, page.click() inside a Then step will not throw — it will silently corrupt the scenario's causal chain. A third, subtler mistake: using But as a negative Then when the assertion is actually a primary outcome, not a contrast. But should feel like "however" — if it reads like "also," use And.

What Most Teams Get Wrong About Keyword Semantics

The most persistent myth is that keyword choice is a style preference with no functional consequence. It isn't. Keyword semantics directly affect how AI-assisted tools and documentation generators interpret scenarios. When you feed a Gherkin suite to ChatGPT or Claude to generate step stubs or living documentation, the model uses keyword position as a signal for intent. A When that reads like a precondition will produce a step stub with setup logic; a Then that navigates will produce an action method. The downstream noise compounds quickly — especially in AI-generated step contexts where context bleed between generated steps breaks scenario isolation in ways that are hard to trace.

A second misunderstanding: teams treat And as a way to avoid writing new step definitions. "And the user is logged in" reuses an existing step — fine. But "And the payment gateway returns a 402" is a new event that deserves its own When or a separate scenario. Chaining unrelated events with And because the step definition already exists produces scenarios that test multiple behaviors simultaneously, making failure attribution ambiguous. The test pyramid debate about how many BDD scenarios to write is real, but the keyword precision question is orthogonal — even a lean suite of 40 scenarios needs correct semantics to remain maintainable at the 18-month mark.

Keyword discipline is a low-cost, high-leverage audit. Run a grep for When steps that contain words like "seeded," "configured," or "enabled" — those are almost certainly misplaced Givens. Check every Then step definition for calls that mutate state or navigate. If you're integrating this into CI, the next thing worth measuring is how keyword precision correlates with mean-time-to-diagnose on failing scenarios. A well-structured scenario should point at the layer of failure before you open a single log file. For the broader pipeline context, see how to integrate BDD into CI/CD without sacrificing speed.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles