BDD vs TDD: One Small Feature, Two Ways to Test It

BDD vs TDD is easier to understand with one small feature than with definitions. This post tests the same delivery rule twice, once as a unit test and once as a plain-language scenario. You’ll see who each test is written for, and when each one earns its place.

Gouache painting: a tea shop counter where one small wooden crate is checked twice: once close up under a magnifying lens, and once from across the room on a wide shelf; the crate's tag is vermilion

The Feature

An online tea shop wants one rule. Orders of $35 or more ship free, and smaller orders pay $4.99 for delivery. It’s small on purpose. A small rule keeps the comparison honest.

Both methods are test first. You write the check before the code, watch it fail, and then write the code that makes it pass. TDD means test-driven development. BDD means behavior-driven development. The difference is not when the test is written, it’s who the test is written for.

The TDD Way: Tests for One Function

TDD starts with the smallest piece of code, here a function called delivery_fee. A developer writes a few automated tests for it. The most important test sits on the boundary: what happens at exactly $35?

import unittest
from shipping import delivery_fee

class DeliveryFeeTests(unittest.TestCase):
    def test_small_order_pays_the_fee(self):
        self.assertEqual(delivery_fee(34.99), 4.99)

    def test_order_of_exactly_35_ships_free(self):
        self.assertEqual(delivery_fee(35.00), 0)

    def test_large_order_ships_free(self):
        self.assertEqual(delivery_fee(80.00), 0)

Now comes the step people skip: watch the tests fail. I first wrote a stub that returns 4.99 for every order and ran the file with Python’s built-in unittest. Two of the three tests failed, as they should. A test that has never failed has never proved anything. The run printed this. It is output, not code to run.

AssertionError: 4.99 != 0
Ran 3 tests
FAILED (failures=2)

Next, the smallest code that passes. Then you clean it up and run the tests again. Developers call this loop red, green, refactor.

def delivery_fee(order_total):
    if order_total >= 35:
        return 0
    return 4.99

With the real function in place, the same run reported Ran 3 tests in 0.000s and OK. Notice what this test knows: a function name, a float and an exact comparison. It’s written by a developer, for a developer.

The BDD Way: A Scenario Anyone Can Read

BDD starts one step earlier, with a conversation. The shop owner, a tester and a developer agree on examples of the rule in everyday words. They write the examples in Gherkin, a plain-text format built around the keywords Given, When and Then.

Feature: Free delivery
  Scenario: An order just below the free delivery amount
    Given a customer has tea worth $34.99 in the cart
    When they go to checkout
    Then delivery costs $4.99

  Scenario: An order at the free delivery amount
    Given a customer has tea worth $35.00 in the cart
    When they go to checkout
    Then delivery is free

Given sets the starting point, When is the action, and Then is the result. No function name appears anywhere. The shop owner can read it and say “yes, that’s my rule” or “no, free delivery should start above $35”.

That second sentence is the real product of BDD. The scenario forces the boundary question while changing the rule is still cheap. The code comes after the agreement.

Where the Code Meets the Scenario

A scenario is only a document until something runs it. A BDD tool reads the file and matches each step to a small piece of code. Cucumber does this for Java, JavaScript and Ruby. Behave does it for Python. Reqnroll does it for .NET, as the successor to SpecFlow. I did not run the next block, because behave isn’t installed on my test machine.

from behave import given, when, then
from shipping import delivery_fee

@given("a customer has tea worth ${amount:f} in the cart")
def step_cart(context, amount):
    context.total = amount

@when("they go to checkout")
def step_checkout(context):
    context.fee = delivery_fee(context.total)

@then("delivery costs ${fee:f}")
def step_fee(context, fee):
    assert context.fee == fee

@then("delivery is free")
def step_free(context):
    assert context.fee == 0

See the point? The scenario calls the same delivery_fee function that the unit tests call. The two methods don’t compete. The scenario checks the behavior the business agreed to, and the unit tests check the code underneath.

When the Rule Changes

Say the owner decides free delivery should start at $40. In the TDD version, a developer changes the number 35 in the tests and the function. In the BDD version, the owner edits the two amounts in the scenarios. The failing steps then point straight at the code to change.

Both versions fail at the right moment, but they fail for different readers. A red unit test tells a developer which function is wrong. A red scenario tells the whole team that the software no longer does what everyone agreed. That second message is the one a project manager can act on.

One more difference shows up in the names. A developer wrote the unit name test_order_of_exactly_35_ships_free for other developers. The scenario name, “An order at the free delivery amount”, belongs to the business and survives a change of amount. Good names are cheap, and they decide how readable a failing test report is.

Side by Side

QuestionTDDBDD
Who reads the test?DevelopersThe whole team
What does it check?One function or classOne behavior the user sees
Written inCodePlain sentences, then glue code
SpeedFast (3 tests under a millisecond here)Slower, because it runs through more layers
Fails whenThe code is wrongThe behavior differs from the agreement

When Each One Fits

Use TDD for logic with many edge cases: tax, dates, rounding, parsing, anything a developer owns alone. Libraries and internal helpers also fit, because nobody outside the team cares how they work. The tests run in milliseconds, so you can run them after every small change.

Use BDD for rules the business owns and for features where people disagree. Free delivery, refund limits and approval steps are good examples. Each one has an owner who isn’t a developer, and that owner must be able to read the examples.

Most real projects that weigh BDD vs TDD end up using both. A few scenarios describe what the user sees, and many unit tests sit underneath, each covering one edge case.

Card titled BDD vs TDD in One Rule: TDD: tests for one function, read by developers; BDD: Given, When, Then scenarios the whole team reads; Both: test first, watch it fail, then write the code; Pick TDD: many edge cases, a developer owns the logic; Pick BDD: rules the business owns, owner must read them. Tip: Ask who needs to read the test.

The Case Against BDD

You could say that BDD is extra typing. Sometimes the person who asks for the feature is the person who builds it. Then there is no conversation to capture, and a unit test says the same thing in fewer lines. Fair point.

BDD pays off only when someone outside the code reads and agrees with the scenarios. When nobody reads them, they are slow tests with extra typing. In that case, write the unit test and move on.

A Simple Rule

For BDD vs TDD, ask who needs to read the test. If only developers do, write a unit test first. If someone who doesn’t code must agree with the rule, write a scenario first. Then add unit tests for the edge cases below it.

BDD vs TDD is not a contest between two tools, it is a choice between two audiences.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Best Practices, Developer, Software Development, Testing
Previous Post
SQL SERVER – Absolute Beginners 10 Queries
Next Post
SQL SERVER – Fix – Error – The certificate chain was issued by an authority that is not trusted

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.