Writing Good Tests in TDD
Ever run a test suite where everything’s green… but you still don’t trust it?
Have you ever encountered that unreliable test everyone avoids, or noticed a flaky test that occasionally fails but gets ignored due to tight deadlines? Let’s explore why this happens.
In Test-Driven Development (TDD), tests aren’t just a safety net you add later. They’re the steering wheel. But if that steering wheel wobbles, you’ll never enjoy the ride.
Why ‘Good Tests’ Matter in TDD
Good tests are what make TDD possible—not just productive. TDD depends on trustworthy feedback loops. Poor tests lead to false confidence, brittle design, and slowed development. Great tests illuminate design issues early.
Example: Consider this misleading test:
// Misleading: Tests implementation, not behavior
[Fact]
public void Test_Addition()
{
var calc = new Calculator();
Assert.True(calc.Add(2, 2) == 4); // What does this really prove?
}
A better test would be:
[Fact]
public void ReturnsSum_WhenAddingTwoNumbers()
{
var calc = new Calculator();
var result = calc.Add(2, 2);
Assert.Equal(4, result);
}
What makes a ‘Good Test’? The 5 Pillars
| Pillar | Description | Example |
|---|---|---|
| Clarity (Naming) | Test names should describe intent, not mechanics. | |
| Structure (AAA) | Arrange → Act → Assert keeps tests readable. | |
| Focus | One behavior per test, one reason to fail. | |
| Readability > Cleverness | Don’t try to optimize test code. Tests are for humans. | |
| No Logic in Tests | Avoid loops, conditionals, or calculations in assertions. | |
How Tests Drive Design
Every good test you write shapes your API. A hard-to-test design usually means the code is too coupled. If your test setup feels painful—that’s feedback, not failure.
Common Pitfalls and Gotchas
- ❌ Tests that don’t test anything — empty or overly mocked tests.
- ❌ Assertion Soup — multiple unrelated asserts.
- ❌ God Test — testing multiple classes or layers in one go.
- ❌ Logic in Tests — if/else or loops inside tests.
// Bad: God Test
[Fact]
public void TestEverything() {
var service = new Service();
var repo = new Repo();
Assert.True(service.DoWork());
Assert.True(repo.Save());
}
// Good: Split into focused tests
Best Practices in the Wild
Here’s a mini before-and-after:
// Before: Messy
[Fact]
public void TestStuff() {
var calc = new Calculator();
Assert.Equal(0, calc.Add(""));
Assert.Equal(1, calc.Add("1"));
Assert.Equal(3, calc.Add("1,2"));
}
// After: Clear, AAA, focused
[Fact]
public void ReturnsZero_WhenEmptyInput() {
var calc = new Calculator();
var result = calc.Add("");
Assert.Equal(0, result);
}
[Fact]
public void ReturnsSum_WhenNumbersProvided() {
var calc = new Calculator();
var result = calc.Add("1,2");
Assert.Equal(3, result);
}
Readability improves confidence and flow.
Wrap-Up: The Meta-Lesson
When your tests read like documentation, you know you’re doing TDD right.
Reflect: If someone who never saw your code can understand your tests—would they know what the system does?
Next, we’ll look at how to keep your tests clean even when the code depends on other code—through mocks, stubs, and fakes.
✨ 5-Point Test Quality Check
- Does the test name describe intent?
- Is the Arrange-Act-Assert structure clear?
- Does the test focus on one behavior?
- Is the test readable by someone new?
- Is there any logic in the test? (If so, refactor!)
