Test-Driven Development

In test-driven development you write a failing test first, then just enough code to pass it, then tidy up. Follow three short cycles that build a shipping calculator.

Apprentice · Unit Testing · C# · Updated

After this lesson you can

  • Explain the red, green, refactor cycle of test-driven development
  • Write a failing test first, then just enough code to make it pass

Key takeaways

  • Test-driven development repeats red, green, refactor: write one failing test, write just enough code to pass it, then tidy the code while the tests stay green.
  • See each new test fail first. A test that has never failed might not test anything at all.
  • Just enough code means just enough: returning a fixed 5.99m is right until a test asks for more.
  • Run every test after refactoring: if they're still green, the tidy-up broke nothing.

So far you've written tests for code that already exists. Test-driven development (TDD) turns that around: you write the test first, watch it fail, and only then write the code. The tests drive what you build.

Red, green, refactor

TDD is a short cycle, repeated for each requirement:

  1. Red. Write one test for the next requirement. Run it, and see it fail. (Test Explorer shows it red.)
  2. Green. Write just enough code to make it pass. Run every test, and see them all pass.
  3. Refactor. Tidy the code (better names, no repetition) while the tests stay green.

Seeing the test fail first matters. A test that has never failed might not test anything at all: a typo in its check, or the wrong method called. Red proves the test can catch a problem.

An example: a shipping calculator

The requirements for an online store's shipping:

  • Orders under $50.00 cost $5.99 to ship.
  • Orders of $50.00 or more ship free.
  • An empty order ($0) costs nothing to ship.

Cycle 1

Red. The first test, for the first requirement:

[TestMethod]
public void Cost_UnderFiftyDollars_IsFiveNinetyNine()
{
    ShippingCalculator calculator = new ShippingCalculator();

    decimal cost = calculator.Cost(20.00m);

    Assert.AreEqual(5.99m, cost);
}

It doesn't even build yet: there's no ShippingCalculator. That counts as red.

Green. Just enough code to pass. Really, just enough:

public class ShippingCalculator
{
    public decimal Cost(decimal orderTotal)
    {
        return 5.99m;
    }
}

Returning a fixed number feels like cheating, but it's the point: no test asks for anything more yet. The next test will.

Cycle 2

Red. The second requirement:

[TestMethod]
public void Cost_FiftyDollarsOrMore_IsFree()
{
    ShippingCalculator calculator = new ShippingCalculator();

    decimal cost = calculator.Cost(50.00m);

    Assert.AreEqual(0m, cost);
}
Failed Cost_FiftyDollarsOrMore_IsFree
  Error Message:
   Assert.AreEqual failed. Expected:<0>. Actual:<5.99>. 'expected' expression: '0m', 'actual' expression: 'cost'.

Green.

public decimal Cost(decimal orderTotal)
{
    if (orderTotal >= 50.00m)
    {
        return 0m;
    }
    return 5.99m;
}

Both tests pass.

Cycle 3

Red. The empty order:

[TestMethod]
public void Cost_OfAnEmptyOrder_IsZero()
{
    ShippingCalculator calculator = new ShippingCalculator();

    decimal cost = calculator.Cost(0m);

    Assert.AreEqual(0m, cost);
}

It fails: an empty order costs $5.99.

Green, then refactor: the numbers get names, so the rules read like the requirements:

public class ShippingCalculator
{
    private const decimal FreeShippingFrom = 50.00m;
    private const decimal StandardCost = 5.99m;

    public decimal Cost(decimal orderTotal)
    {
        if (orderTotal == 0 || orderTotal >= FreeShippingFrom)
        {
            return 0m;
        }
        return StandardCost;
    }
}

Run all three tests after refactoring: still green, so the tidy-up broke nothing.

Why work this way

  • Every line of code is there because a test asked for it, so every line is tested.
  • You think about what the code should do before how. Writing the test first means reading the requirement closely.
  • Refactoring is safe. The tests tell you at once if a change breaks something.

Many professional teams work this way: no production code without a test that demands it.

Check yourself

2 questions to try on the lesson page.

So what

Red, green, refactor: one failing test, just enough code, then tidy. It feels slow for the first few cycles, then it feels like the safest way to build anything.

Key terms

Cheat sheets