Skip to main content
Clean Code 60 XP · 6 min

Clean Unit Testing

Tests are first-class code — dirty tests become as costly to maintain as production code.

Showing
Ad (728×90)

The Three Laws of TDD

1. Write no production code until you have a failing test. 2. Write no more test than is sufficient to fail. 3. Write no more production code than is sufficient to pass the test. The cycle is: Red → Green → Refactor. Tests written after code are often incomplete — they test what you happened to write, not what the code should do.

// TDD cycle for a discount calculator

// ── Red ─────────────────────────────────────────
function test_no_discount_for_regular_users():
    calc = new DiscountCalculator()
    assert calc.apply(100.0, "regular") == 100.0  // fails — class doesn't exist

// ── Green ───────────────────────────────────────
class DiscountCalculator:
    function apply(price, tier):
        return price  // simplest code to pass

// ── Refactor ────────────────────────────────────
// Next test drives the next behavior:
function test_10_percent_off_for_premium_users():
    calc = new DiscountCalculator()
    assert calc.apply(100.0, "premium") == 90.0

The F.I.R.S.T. Rule

Clean tests are Fast (run in milliseconds — no DB, no network), Independent (no test sets up state for another), Repeatable (same result in any environment), Self-validating (pass or fail — no human reading of output), Timely (written just before the production code). A slow, order-dependent test suite is a test suite developers stop running.

Not FIRST — slow, order-dependent, real DB

class TestUserService:
    function test_create_user():
        db = RealDatabase.connect("localhost:5432")  // slow!
        service = new UserService(db)
        user = service.create("Alice", "a@t.com")
        savedUserId = user.id  // ← shared state!

    function test_find_user():
        user = service.find(savedUserId)
        assert user.name == "Alice"  // depends on prev test

FIRST — fast, isolated, uses a fake

class TestUserService:
    function setup():
        db = new FakeDatabase()   // fast, in-memory
        service = new UserService(db)

    function test_create_returns_user_with_given_name():
        user = service.create("Alice", "a@t.com")
        assert user.name == "Alice"

    function test_find_returns_null_for_unknown_id():
        result = service.find("unknown-id")
        assert result == null

One Assertion Per Test

Each test function should verify one concept. Multiple assertions per test mean multiple failure modes — when it fails, you don't know which concept broke. Use descriptive test names as documentation: test_inactive_users_cannot_place_orders tells you the rule being tested. Future developers will read tests to understand behavior, not source code.

One test, six assertions, six failure points

function test_user():
    user = new User("alice", "alice@test.com")
    assert user.name == "alice"
    assert user.email == "alice@test.com"
    assert user.isActive == true
    user.deactivate()
    assert user.isActive == false
    user.setRole("admin")
    assert user.role == "admin"

One concept per test

function test_user_is_active_by_default():
    user = new User("alice", "alice@test.com")
    assert user.isActive == true

function test_user_becomes_inactive_after_deactivation():
    user = new User("alice", "alice@test.com")
    user.deactivate()
    assert user.isActive == false

function test_user_role_can_be_set():
    user = new User("alice", "alice@test.com")
    user.setRole("admin")
    assert user.role == "admin"

Code Challenge

Split the God Test — this test has 5 different concepts. Split each into its own function with a descriptive name that reads as a rule.

Key takeaway

Tests are the first client of your code. Clean, fast, focused tests make fearless refactoring possible.

Done with this lesson?

Mark it complete to earn XP and track your progress.