Test code quality
Quick recap
- Untestable code is a design smell
- Seams: where a test can substitute behaviour, and the enabling point that lets it
- Controllability and observability
The claim
Everything that follows argues for two sentences:
- Test code is code. It rots the same way production code rots, and for the same reasons.
- A suite you have stopped trusting is worse than no suite. No suite is an absence. An untrusted suite is a cost you keep paying for a guarantee you no longer believe.
The first is easy to accept and easy to forget. The second is what makes the first matter.
Agenda
- Test code is code
- Five smells, with instances
- Determinism: the clock, the dice, the keyboard
- Tests that pass without testing anything
- Fixtures, and what they reveal about the design
1. Test code is code
You wrote a test once. You will read it every time it fails, and every time you are not sure whether it failed for a real reason.
- Production code is read to find out what the system does.
- Test code is read under pressure, at the worst moment, to find out whether you are in trouble.
A test that cannot answer that question quickly has failed at its job even when it is green.
Write the test for the person who will read it at 6pm on a Friday.
Forty-three lines, four tests, five smells
The whole file β tests/test_retrieval.py:
@pytest.fixture
def retriever():
return PostgresFullTextRetriever()
def test_retrieve_faculty_basic(retriever):
"""Test retrieving faculty returns a list of dictionaries with correct keys."""
results = retriever.retrieve_faculty("machine learning", limit=5)
assert isinstance(results, list)
if results:
assert "name" in results[0]
assert "specialization" in results[0]
assert "relevance_score" in results[0]
def test_retrieve_respects_limit(retriever):
"""Test that the retrieval limits are respected."""
# Assuming there are at least 3 faculties in DB
results = retriever.retrieve_faculty("a", limit=2)
assert len(results) <= 2
What each line costs
| Line | Smell | Why it hurts |
|---|---|---|
PostgresFullTextRetriever() |
Mystery guest | The data it depends on is not in the file. It is in a database, on a machine, seeded by someone. |
# Assuming there are at least 3 faculties in DB |
Resource optimism | The assumption is written down and unenforced. A comment is not a fixture. |
assert isinstance(results, list) |
Vague assertion | Passes on an empty list. Passes if retrieval is deleted and replaced with return []. |
if results: |
Conditional assert | Covered in 7-8. An empty database turns three assertions into zero. |
len(results) <= 2 |
Assertion that cannot fail usefully | Zero is <= 2. The limit is never actually tested. |
Run this against an empty database. Four tests pass. Nothing has been verified.
The same repository, done well
tests/test_library.py β 19 tests, four classes, one behaviour each:
async def test_limit_and_offset_reach_the_query(self, mocker):
"""Paging is done in SQL, so limit/offset must be passed through."""
cur = mock_db(mocker, fetchall=[])
await LibraryService().search_books("python", limit=5, offset=10)
_sql, params = cur.execute.call_args.args
assert params[-2:] == (5, 10)
- The dependency is supplied, not hoped for.
- The docstring says why the test exists, not what the code does.
fetchall=[]β the empty case is deliberate here, and the assertion still bites.
Same code base. Same team. The difference is not skill.
π¬ Two suites, one repository. What is actually different?
test_retrieval.py and test_library.py were written against the same kind of
thing: a service that runs SQL and returns rows. One is worthless, one is good.
Name the single decision that separates them.
Answer
One of them controls its dependency. The other hopes for it.
Every smell in the table follows from that. Once the database is real and
unseeded, you cannot write a sharp assertion β you do not know what is in
there. So you soften: isinstance, if results:, <= 2. Each softening is
locally reasonable and the sum is a suite that verifies nothing.
The vague assertions are not the disease. They are the symptom of an uncontrolled dependency. This is 13-14 again, arriving from the other side: the missing seam shows up first as a bad test, not as bad code.

2. Five smells
Mystery guest
The test depends on something it does not show you.
pytest.skip("data/Lab Data.xlsx not present")
Honest about it, which is better than most. But the behaviour is still: on your machine the test runs, on mine it vanishes, and the suite reports success either way.
Fix: build the input in the test, or commit a small fixture file next to it.
Resource optimism
The test assumes an environment it did not create.
test_fuzzy_directory_search.py:
@pytest.fixture
def patched_db(self):
import psycopg2
conn = psycopg2.connect(**_DB_KWARGS) # a real server, right here
This is a legitimate integration test. The problem is that it sits in the same suite, behind the same command, as the unit tests β so one unavailable server makes the whole run red and people learn to ignore red.
Fix: separate the run. Not the technique β the button.
General fixture
One SetUp serving every test, most of it unused by any single one.
snake_test.cpp:
class SnakeTest : public ::testing::Test {
protected:
void SetUp() override { stub::reset(); lup = 0; sc = 0; run = true; }
};
lup, sc, run are file-scope globals in the game. The fixture is not organising the
test β it is undoing the previous test.
A fixture that resets globals is a design report, not a test utility.
Compare the DAU-buddy version, where mock_db(mocker, fetchall=...) takes an argument
because each test wants different rows. One is a helper. The other is a cleanup crew.
Eager test
test_timetable_mcp_availability.py β 198 lines. One test function.
It builds an in-memory SQLite database, inserts a timetable, patches a service, runs the overlap query, and asserts. When it goes red you have 198 lines and no idea which of them is the reason.
Fix: the arrangement is fine. Split the acts.
Assertion roulette
A test with many bare assertions and no messages. It fails at line 42 and the report says: line 42.
assert result["total_matches"] == 10 # which one broke?
assert result["showing"] == "1-1"
assert result["more_available"] is True
Here the names are self-describing, so it is survivable. It stops being survivable when the values are positional, or the same assertion runs in a loop.
Fix: name the thing, or assert on a whole structure at once so the diff shows you everything that moved.
3. Determinism
A test that can fail without the code changing is not a test. It is a rumour.
Across the 36 snake repositories:
| Dependency | Repos |
|---|---|
sleep / Sleep() / sleep_for |
36 |
getch() |
32 |
kbhit() |
30 |
rand() |
30 |
srand() |
30 |
system("cls") / system("clear") |
25 |
fstream |
22 |
Every one of these is a hard-coded dependency on something the test cannot control: the clock, the dice, the keyboard, the terminal, the disk.
This is why none of these repositories have tests. Not because nobody tried. Because there was nothing to hold.
The same picture, twice

The left panel is every repository in the table above. The right panel is what the harness in this lecture had to build before it could test one.
Nothing on the right is cleverer. The arrows point the other way, and that is the whole difference.
What the harness did about it
stub::time = 0.5;
EXPECT_TRUE(eventTrigger(0.3));
EXPECT_DOUBLE_EQ(lup, 0.5);
stub::randomValues = {0,0, 5,7}; // first (0,0) is on snake -> retry
Food f({{0,0},{1,0}});
EXPECT_EQ(f.p.x, 5); EXPECT_EQ(f.p.y, 7);
stub::keys = {{KEY_UP,true},{KEY_DOWN,true},{KEY_LEFT,true},{KEY_RIGHT,true}};
A scripted clock, scripted dice, a scripted player. The second one is the good test in the file: it proves the retry branch β that a fruit landing on the snake is rejected β and it can only prove it because the dice are scripted.
That is 13-14 Β§2 arriving as a fact rather than a slide.
π¬ Your snake uses rand(). Prove fruit never spawns on the snake.
The rule is one line of code and everybody has it. Write the test.
Answer
You cannot, and the reason is not the test.
With rand() called inside place_fruit(), the only available test is "run it
five hundred times and check it never happened." That test is slow, it is
probabilistic, and when it fails it cannot tell you which call did it.
Inject the source of randomness and the test becomes three lines with an exact
expected value β the stub::randomValues example above.
The untestable part was never the rule. It was the dependency the rule reached out and grabbed.
4. Tests that pass without testing
The most expensive test is not the one that fails. It is the one that has never failed and never could.
Read this one carefully
TEST_F(SnakeTest, MainLoopKeyPresses) {
// 4 frames, each with a different key; direction rules exercised
stub::framesUntilClose = 4;
stub::randomValues = {10,10};
stub::keys = {{KEY_UP,true},{KEY_DOWN,true},{KEY_LEFT,true},{KEY_RIGHT,true}};
EXPECT_EQ(snake_main(), 0);
}
The comment says the direction rules are exercised. They are β the code runs.
Nothing about direction is asserted. The only assertion is that main returned 0.
So delete the rule. All four guards, in the game:
if(IsKeyPressed(KEY_UP) && (s.direction.y!=1)) -> if(IsKeyPressed(KEY_UP))
if(IsKeyPressed(KEY_DOWN) && (s.direction.y!=-1)) -> if(IsKeyPressed(KEY_DOWN))
if(IsKeyPressed(KEY_LEFT) && (s.direction.x!=1)) -> if(IsKeyPressed(KEY_LEFT))
if(IsKeyPressed(KEY_RIGHT) && (s.direction.x!=-1)) -> if(IsKeyPressed(KEY_RIGHT))
The snake can now reverse into itself. Rebuild and run:
[==========] 19 tests from 1 test suite ran.
[ PASSED ] 19 tests.
Not one test noticed. Not just MainLoopKeyPresses β the entire suite is blind to a
rule the game cannot be played without.
Coverage goes up. Verification does not exist.
It can only confirm that the code does what it does.
Leaking the implementation into the test
// Head starts at x==1 moving right. With time advancing 1s per frame every
// frame triggers an update, so the head reaches x==cellcount on frame 24.
stub::framesUntilClose = 30;
The test encodes an arithmetic derivation about the game's internals β the head must reach the wall inside the 30 frames the test allows.
Widen the board and re-run. cellcount is a pure configuration constant; no behaviour
changes:
cellcount |
Result |
|---|---|
| 25 (as shipped) | 19 passed |
| 29 | 19 passed |
| 31 | 19 passed |
| 32 | MainLoopHitsRightWallAndResets fails |
| 35 | same one fails |
Nothing broke at 32. The snake simply ran out of frames before reaching the wall, so
go() was never called and the score was never reset.
The test has a silent tolerance window β it holds to 31 and not past it. That number
appears nowhere. Nobody chose it. It is a side effect of framesUntilClose = 30.
That is a false positive: red without a defect. Lecture9_10.md called it the worse
of the two failures, and this is what it looks like in the wild.
Fix: assert the event, not the frame count. Run until the wall is hit, then check.
The assertion that was weakened to make it pass
EXPECT_GE(stub::soundPlays, 1); // wall sound
GE where EQ was meant. Somebody wrote EXPECT_EQ(stub::soundPlays, 1), it failed
because the loop played the sound twice, and the fastest green was to relax the operator.
Now the test passes if the wall sound plays once, or forty times.
Every weakened assertion is a bug report somebody chose not to read.
Testing private methods
DAU-buddy does this four times:
| Test | Calls |
|---|---|
test_calendar.py#L127 |
calendar_service._parse_day_substitution() |
test_chat_guardrails.py#L43 |
gemini._extract_function_calls() |
test_seed_timetable.py#L263 |
seed_timetable._parse_section_header() |
test_seed_timetable.py#L296 |
seed_timetable._filter_meta() |
The usual rule says do not test private methods. Apply it here and you delete some of the best tests in the repository.
π¬ Four tests call a _private function. Which rule is wrong?
Either the tests are wrong, or the rule is, or the code is. Pick one and defend it.
Answer
The underscore is wrong.
Look at what all four have in common: they are pure functions. String in, tuple out. No state, no I/O, no collaborators.
And they have real edge cases. _parse_section_header does not just split a
string β it normalises the programme name, and the workbook it parses ships
with typos:
| Input | Output |
|---|---|
"BTech (MnC) Core: SEMESTER III" |
("B Tech (MnC)", 3) |
"BTech (ICT & CS) Elective: SEMESTER V (2024 Batch)" |
("B Tech (ICT and CS)", 5) |
"MSc (DS) Coe : SEMESTER III (2025 Batch)" |
("MSc (DS)", 3) |
"Some New Programme: TERM 2" |
(None, None) |
BTech becomes B Tech. & becomes and. Coe is a typo for Core and is
tolerated. One test is named test_longest_key_wins because BTech is also a
key and the more specific match has to take precedence.
A pure function with its own input space and its own edge cases is a unit.
The _ says "implementation detail of this module", and for these four it is a
lie β they have a contract worth stating and worth protecting.
The rule is right about why: do not reach into a class to observe state the public interface deliberately hides. That is the Fix 1 anti-pattern from 13-14. These four are not that. They are a module that has not yet admitted what its interface is.
Ask whether the thing has a contract, not whether it has an underscore.
5. Fixtures
Two fixtures, from the two code bases:
def mock_db(mocker, *, fetchall=None, fetchone=None):
"""Point library_service.db_connection at a mock cursor."""
void SetUp() override { stub::reset(); lup = 0; sc = 0; run = true; }
mock_db |
SetUp |
|
|---|---|---|
| Takes arguments | yes β each test asks for its own rows | no |
| What it does | supplies a dependency | resets globals |
| If the design improved | unchanged | unnecessary |
A fixture that would disappear if the design were better is telling you about the design.
mock_db is not going anywhere; injecting a cursor is what the test wants to do.
SetUp exists only because lup, sc and run are file-scope mutable state, and it
will exist for exactly as long as they are.
π¬ Open a test you have written. Which row of the catalogue is it?
Not a trick question. Pick one test, name the smell, name the fix. If you cannot find a smell, name the assertion that would fail if you deleted the feature.
What to look for
- Does it touch a file, a clock, a network, or a database it did not create?
- Delete the feature it tests. Does it go red?
- Change something cosmetic β a rename, a constant. Does it go red?
- How many behaviours does it check?
- When it fails, does the message tell you what broke?
The second and third are the important ones, and they pull in opposite directions. A test must fail when behaviour changes and must not fail when it does not. Most bad tests fail the first check. The expensive ones fail the second.
Back to the claim
Every smell in this lecture traces to one of two roots.
| Root | Smells it produces |
|---|---|
| The dependency could not be controlled | mystery guest, resource optimism, vague assertion, conditional assert, general fixture |
| The assertion was written to pass, not to catch | weakened operator, leaked implementation detail, no assertion at all |
The first root is 13-14's subject arriving one lecture late: a missing seam shows up as a bad test before it shows up as bad code.
The second is nobody's design failing. It is what happens when a red build is an obstacle instead of information.
Test code is code β and a suite you have stopped trusting is worse than no suite, because you are still paying for it.
References:
- Chapter 10, Test Code Quality β Effective Software Testing, MaurΓcio Aniche
- Chapter 11, Test Code Smells and Anti-patterns β Effective Software Testing, MaurΓcio Aniche
- xUnit Test Patterns β test smells catalogue, Gerard Meszaros