1. Home
  2. Blog
  3. Test Automation

Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD helps developers design correct code. BDD helps a whole team agree on what the software should do. See the same discount feature built with each approach, and learn how the two work together.

9 min read
TDD vs BDD: key differences, one feature built both ways and when to use each

Key takeaways

  • TDD means writing a failing test first, then just enough code to pass, then tidying up.
  • BDD describes behaviour in shared language, usually as Given, When, Then.
  • TDD mainly guides code design. BDD mainly builds shared understanding.
  • They are not rivals, and many teams use BDD for features and TDD for the details beneath.

The TDD vs BDD debate often gets framed as choosing a side. In reality, they answer different questions. Test-driven development asks whether the code does what the developer intended, while behaviour-driven development asks whether the team agrees on what the software should do at all.

To make the difference concrete, we build one small discount feature with each approach. We ran the code on Python 3.13, using the standard library, and the results below come from that run. The BDD part uses a tiny runner of our own to show the mechanism, so a real project would use a tool such as Cucumber or behave instead.

The short answer

Test-driven development (TDD) is a coding practice. You write a small failing test, make it pass with the simplest code, and then improve the design. Tests are written in code, and mainly by developers. Kent Beck popularized the practice.

Behaviour-driven development (BDD) is a collaboration practice that grew out of TDD, and Dan North introduced the term. Developers, testers and business people agree on examples of how the system should behave, written in plain language, and those examples can then be automated.

TDD vs BDD side by side

Aspect TDD BDD
Main focus Correct, well-designed code Shared understanding of behaviour
Written in Programming language Plain language, such as Gherkin
Written by Developers Business, QA and developers together
Readable by Developers Everyone on the team
Typical level Unit Feature or acceptance
Starting point A failing test A conversation with examples
Cycle Red, green, refactor Discover, formulate, automate
Common tools pytest, JUnit, Jest Cucumber, behave, SpecFlow, Reqnroll

The feature

Here is the business rule we will implement: Orders of 1000 or more get 10% off. Members get an extra 5% off those orders.

That may sound clear, but it is already ambiguous. Does a member spending 500 get 5% off? The sentence says “those orders”, so we assume not. Keep that in mind, because this is exactly the kind of question BDD is designed to bring out early.

Building it with TDD

TDD follows a short loop known as red, green, refactor:

Step What you do
Red Write one test for behaviour that does not exist yet, and watch it fail
Green Write the simplest code that makes it pass
Refactor Improve the code, keeping the tests green

These are our tests, with one for each rule:

class DiscountTests(unittest.TestCase):
    def test_no_discount_below_1000(self):
        self.assertEqual(discount(999, False), 0)

    def test_ten_percent_from_1000(self):
        self.assertEqual(discount(1000, False), 100)

    def test_member_gets_extra_five_percent(self):
        self.assertEqual(discount(1000, True), 150)

We then wrote the code in three stages, running the tests after each. A strict TDD session adds one test at a time, and this shows the same progression:

Stage 1: the simplest thing

def discount(total, is_member):
    return 0

# 3 tests, 2 failed
#   test_member_gets_extra_five_percent
#   test_ten_percent_from_1000

Returning zero looks silly, but it proves the tests can actually fail, and the one test it passes is the “no discount” rule.

Stage 2: add the threshold

def discount(total, is_member):
    if total >= 1000:
        return total * 0.1
    return 0

# 3 tests, 1 failed
#   test_member_gets_extra_five_percent

Stage 3: add members, and clean up

def discount(total, is_member):
    rate = 0.1 if total >= 1000 else 0
    if is_member:
        rate += 0.05
    return round(total * rate, 2)

# 3 tests, 0 failed

All three pass. But read this version closely. It gives a member spending 500 a 5% discount, which is not what we assumed the rule meant. No test covered that case, so nothing flagged it. TDD only protects the behaviour you thought to test. That is a real limitation, and the reason conversations about examples matter so much.

Why the cycle matters

  • Red proves that the test works. A test that has never failed might not be testing anything.
  • Green keeps you focused. You write only the code that is needed, which helps prevent over-engineering.
  • Refactor keeps quality high. Tests give you the confidence to change the design safely.
  • It influences design. Code that is hard to test is often too tightly coupled, and you notice right away.

Our pytest tutorial shows a more convenient way to write tests like these, and boundary value analysis explains why we chose 999 and 1000 as values.

Building it with BDD

BDD starts with a conversation, not with tools. Often it is a short meeting known as the three amigos, with a product person, a developer and a tester discussing concrete examples:

Tester:  What about a member spending 500?
Product: Good question. No discount. It has to reach 1000 first.
Dev:     And exactly 1000 counts?
Product: Yes, 1000 or more.

That one exchange would have caught the problem in our TDD code. They then record their examples in a structured format called Gherkin:

Feature: Order discounts

  Scenario: Member reaches the discount threshold
    Given a member with a cart worth 1000
    When they view the amount to pay
    Then the amount to pay is 850

  Scenario: Member below the threshold gets no discount
    Given a member with a cart worth 500
    When they view the amount to pay
    Then the amount to pay is 500
Keyword Purpose
Given The starting situation
When The action or event
Then The expected result

Anyone on the team can read that and say “yes, that is right” or “no, that is wrong”. Even if you never automate it, the scenario has already done useful work.

Connecting steps to code

Tools connect each plain-language line to a function, called a step definition. To show the idea, we wrote a tiny version ourselves and ran the first scenario:

@step("a member with a cart worth 1000")
def given(ctx):
    ctx["cart"] = Cart(is_member=True)
    ctx["cart"].add(1000)

@step("they view the amount to pay")
def when(ctx):
    ctx["amount"] = ctx["cart"].payable()

@step("the amount to pay is 850")
def then(ctx):
    assert ctx["amount"] == 850

# ok Given a member with a cart worth 1000
# ok When they view the amount to pay
# ok Then the amount to pay is 850

Real tools add parameters, so that a single step can match both “1000” and “500”, plus reporting. The result is living documentation: a plain description of the system that fails loudly if it stops being true.

Tip: The value of BDD lies in the conversation. Teams that have one person write Gherkin alone after the code is finished get the cost of the tool, with almost none of the benefit.

Writing good scenarios

Weak Better
When I click the button with id "btn-77" When I apply the coupon
Then the result is correct Then the amount to pay is 850
Twenty steps in one scenario One behaviour per scenario
Describes the screen Describes the business intent

Scenarios that describe clicks and field IDs break on every redesign, and business readers stop reading them. Keep the user interface details in the step code. These scenarios are close cousins of the examples in test case vs test scenario, and they are often used in user acceptance testing too.

Using both together

The two fit naturally into an “outside-in” workflow:

  1. The team agrees on behaviour, and writes a BDD scenario. It fails, because nothing exists yet.
  2. A developer uses TDD to build the small units needed.
  3. Unit tests pass, and eventually so does the scenario.
  4. Everything runs in your CI/CD pipeline.

Keep each at the right level. You should have a few scenarios for important behaviours, and many fast unit tests for the details. Our guide to unit vs integration testing covers the balance.

Tools

Language TDD (unit testing) BDD
Python pytest, unittest behave, pytest-bdd
JavaScript Jest, Vitest Cucumber.js
Java JUnit, TestNG Cucumber-JVM
.NET xUnit, NUnit Reqnroll, SpecFlow

When to use each

TDD fits well when:

  • You are writing complex logic, like pricing, parsing or permissions.
  • You are fixing a bug, since you can reproduce it first with a failing test.
  • You are working on code that will change often.

BDD fits well when:

  • Business rules are complex, and misunderstandings are expensive.
  • Non-technical people really will read and discuss the scenarios.
  • Several teams need a shared definition of how something behaves.

Either may be overkill when: you are writing a throwaway prototype or exploring an unclear technical idea. Many developers spike first, then rewrite with tests. BDD tooling is also a poor investment if nobody outside engineering will ever look at the scenarios. In that case, plain tests with good names are simpler.

Common mistakes

  • Treating BDD as a testing tool. Cucumber without collaboration is just a verbose way to write tests.
  • Testing implementation in TDD. Tests tied to private details break during every refactor.
  • Skipping the refactor step. You get passing tests on top of messy code.
  • Writing tests after the code and calling it TDD. That is still useful, but you lose the design feedback.
  • Writing too many UI scenarios. These become slow and flaky. See flaky tests.
  • Chasing coverage numbers. Good coverage still misses behaviours nobody considered, as our example shows.

Final thoughts

TDD helps you build the thing right, and BDD helps you build the right thing. Our discount example showed both sides: the code passed all its tests, yet a simple conversation revealed a wrong assumption. Try it on your next small feature. Write three concrete examples with a colleague before any code, and then build it one failing test at a time.

Frequently asked questions

What is the main difference between TDD and BDD?

TDD focuses on developers writing failing code tests before implementation. BDD focuses on a whole team agreeing on behaviour using plain-language examples.

Can you use TDD and BDD together?

Yes, and many teams do. BDD scenarios define features from the outside, and TDD drives the smaller units of code beneath them.

Is BDD better than TDD?

Neither is better overall, as they solve different problems. BDD helps with shared understanding, and TDD helps with code design and fast feedback.

What is Gherkin?

Gherkin is a structured plain-language format for writing scenarios, using keywords such as Given, When and Then. Tools like Cucumber can run it.

Does TDD slow development down?

It can feel slower at first, especially while learning. Teams often regain time through fewer bugs and safer changes, though the effect varies by project.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →