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.
- 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:
- The team agrees on behaviour, and writes a BDD scenario. It fails, because nothing exists yet.
- A developer uses TDD to build the small units needed.
- Unit tests pass, and eventually so does the scenario.
- 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
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.