Backend / Testing / Strategy / 05_bdd_and_specification_by_example.md

BDD, ATDD and Specification by Example

Updated 4 interview angles 4 min read source
On this page6
  1. The same loop, one level up
  2. In Python
  3. When it pays, and when it is theatre
  4. Where it breaks in production
  5. Related
  6. Interview angle

BDD, ATDD and Specification by Example

“TDD or BDD?” is asked as if they competed. They are the same loop at different altitudes, and the interview answer is knowing what BDD adds, what it costs, and the one condition under which it pays for itself.

The same loop, one level up

TDD’s inner loop is write a failing test, make it pass, refactor. BDD keeps the loop and changes the unit: instead of a test named test_discount_applies_over_threshold, a scenario written in language the business can read.

gherkin
Feature: Order discounts

  Scenario: Orders over the threshold get the bulk discount
    Given a customer with an empty basket
    When they add 12 units of "widget" at 10.00
    Then the order total is 108.00
    And the discount line reads "bulk 10%"

Given is the state, When the action, Then the observable outcome. The format is Gherkin, and the feature file is meant to be the specification the product owner signs off — which is the entire point, and also the entire cost.

ATDD / Specification by Example is the acceptance-level version: the examples are agreed with stakeholders before work starts and become the definition of done. BDD is the practice; Specification by Example is the book that says why (Adzic).

In Python

python
# steps/orders.py - pytest-bdd binds each line to a function.
from pytest_bdd import given, when, then, scenarios, parsers

scenarios("../features/discounts.feature")

@given("a customer with an empty basket", target_fixture="basket")
def empty_basket():
    return Basket()

@when(parsers.parse('they add {qty:d} units of "{sku}" at {price:f}'))
def add_units(basket, qty, sku, price):
    basket.add(sku, qty, Decimal(str(price)))

@then(parsers.parse("the order total is {total:f}"))
def check_total(basket, total):
    assert basket.total() == Decimal(str(total))

pytest-bdd keeps you in pytest — fixtures, plugins, the same runner. behave is the standalone alternative with its own runner and a closer match to Cucumber’s conventions. Choose pytest-bdd unless the team already runs Cucumber elsewhere and wants one dialect.

The step definitions are the tax. Every phrasing in a feature file needs a binding, so a wording change breaks a step, and a team that writes scenarios freely ends up with hundreds of near-duplicate step functions.

When it pays, and when it is theatre

BDD earns its cost under exactly one condition: someone who is not an engineer reads the scenarios. A product owner who reviews feature files, argues about an example and catches a misunderstanding before code exists — that is the return, and it is large.

Without that reader, Gherkin is an indirection layer over pytest: the same tests, harder to write, harder to refactor, with a parser in the middle. Most teams that adopt BDD end up here, because the business reader was assumed rather than confirmed. It is worth saying in an interview that you have seen this failure, because it is the common one.

Signal BDD is
Product reviews scenarios earning its cost
Engineers write both sides pytest with extra steps
Scenarios mirror unit tests duplicated

The examples matter more than the format. The lasting idea from Specification by Example is the conversation — three amigos (product, dev, test) working through concrete examples until the edge cases surface. That conversation is valuable whether or not the result is a Gherkin file; a table of examples in a ticket captures most of it.

Where it breaks in production

  • Scenarios as unit tests. A feature file asserting on internal method behaviour is a unit test wearing a costume. Keep BDD at the behaviour boundary — an API, a workflow — and unit-test underneath with pytest.
  • Step-definition sprawl. Free-form wording yields one binding per phrasing. Agree a small vocabulary and reuse it.
  • Slow suites. Scenarios tend to drive the whole stack, so they sit at the top of the pyramid and need the same restraint as E2E tests — see Test Strategy and the Test Pyramid.
  • Nobody reads the report. The living-documentation output is only documentation if someone opens it.

Interview angle 4

  • “TDD or BDD?” - the same loop at different altitudes. TDD is the developer’s inner loop; BDD adds a scenario a non-engineer can read, and it pays for itself only when that non-engineer actually reads it. Follow-up: what is ATDD? Acceptance examples agreed with stakeholders before the work starts - BDD’s definition-of-done cousin.
  • “When is BDD a mistake?” - when engineers write both the feature file and the steps and nobody else looks. Then it is pytest with a parser in the middle, harder to refactor, and the step definitions sprawl.
  • “pytest-bdd or behave?” - pytest-bdd to stay inside pytest with its fixtures and plugins; behave when the team already speaks Cucumber and wants one dialect across languages.
  • “What survives if you drop Gherkin?” - the conversation. Working through concrete examples with product and test until the edge cases appear is the value; a table of examples in the ticket keeps most of it.