Lesson 2 of 9 · 20m
Unit Tests That Don't Rot
Arrange-Act-Assert, test isolation, and the assertion patterns that survive refactors instead of breaking on them.
A unit test has one job: verify a piece of logic does what it says when everything else is held constant. Tests that rot are tests that fail when the code is right — usually because they assert on implementation details rather than behavior.
The shape that survives
def test_discount_applies_to_subtotal():
# Arrange — the minimal fixture that makes the test readable
cart = Cart(items=[Item(price=100), Item(price=50)])
# Act — one operation under test
cart.apply_discount(0.10)
# Assert — the observable result, not the mechanism
assert cart.total == 135AAA isn't a style — it's a readability contract. If your Arrange section is 40 lines, the test is telling you the object is too coupled; if Assert checks internals (cart._discount_rate == 0.10), the test couples to implementation and will break on refactor.
What makes a unit test rot
- Asserting on internals —
mock.assert_called_with(db.execute, 'SELECT ...')breaks on a harmless refactor. Assert the outcome (result.total == 135), not the path. - Testing too much at once — one test that exercises five behaviors fails ambiguously; five focused tests point at the failure.
- Depends on time, randomness, or order — inject the clock, seed the RNG, sort deterministically.
Factories beat fixtures for objects
Fixture files grow into rigid hell — a user with every field set when you only need email. A factory (UserFactory(email='x')) sets the minimum and fills the rest with defaults: readable, composable, and the test stays valid when the model gains a new required field.
The unit test isn't a function call
"Unit" is about what's under test — a function, a class, a module — not the test's shape. A unit test exercises the code path without touching the database, network, or filesystem; when it touches any of those, it's an integration test, and it should live with the integration suite.
Field notes
- Test names should read like requirements:
test_rejects_expired_tokenbeatstest_auth_3. - Parametrized tests (
@pytest.mark.parametrize) compress N near-identical tests into a table — write them when the shape repeats. - A test that can't fail is worse than no test — assert something real, or delete the test.