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.99mis 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:
- Red. Write one test for the next requirement. Run it, and see it fail. (Test Explorer shows it red.)
- Green. Write just enough code to make it pass. Run every test, and see them all pass.
- 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.