Tests that notice a fault
Week 7 · A green run is evidence only for the faults the tests could have noticed
Three questions for a green run
- Which planted fault would these tests miss?
- Which input did nobody think of?
- Did the code change, or did the test?
All examples today are course-written and rerunnable.
The contract comes first
accepts: exactly one of "0" ... "9"
ensures: the corresponding integer
rejects: every other string with ValueError
Every expected value comes from these lines.
The candidate
def parse_level(text: str) -> int:
if text[0] in "0123456789":
return int(text[0])
raise ValueError("Invalid level")
Predict two rejected inputs it mishandles.
A suite that cannot fail
result = parser("3")
assert result == result
assert parser
assert result or True
demo.py weak exits 0 on the faulty candidate.
A suite with expected values
parser("3") == 3, and "", "12", "x" must raise ValueError.
demo.py unit exits 1: "failure": "IndexError".
A known input, an expected result, a different observed result.
Generate, then verify
Proposal: check len(text) == 1 before reading the digit.
Same tests, unchanged: demo.py unit --implementation repaired exits 0.
Red before, green after, tests untouched.
Green raises the next question
Which faults would this suite have missed?
Plant one and see.
A mutant
A copy of correct code with one small, deliberate fault.
- A test fails: the mutant is killed.
- Every test passes: the mutant survived.
Three mutants of the repaired parser
| Mutant | Fault |
|---|---|
all_zero |
every accepted string returns 0 |
nine_to_zero |
only "9" returns 0 |
accept_trailing |
"12" is accepted as 1 |
An agent's first suite
assert parser("3") == 3
Plus one rejected input: parser("x") raises ValueError.
Reads well. Passes on the repair.
It kills 1 of 3
demo.py survivors, first line:
"all_zero": "killed", "nine_to_zero": "survived",
"accept_trailing": "survived"
A survivor is a gap in the tests
nine_to_zero survives: no test looks at "9".
accept_trailing survives: no rejected input starts with a digit.
Each survivor names the missing observation.
Write the test the survivor asks for
assert parser("9") == 9
Second line of the output: nine_to_zero is killed, 2 of 3.
Then the next one
parser("12") must raise ValueError.
Third line: accept_trailing is killed, 3 of 3.
A new test must do both
Fail on the mutant. Pass on the original.
A test that passes on both closed nothing.
Keep the count honest
- The original passes first.
- State the denominator: which mutants, chosen how.
- A kill is a test failing because of the change; an import error or broken run is not.
- An equivalent mutant is excluded with a written reason.
Coverage would not have shown this
The first suite already runs every line of the repaired parser.
Coverage says a line ran. A mutant asks whether changing it was noticed.
From examples to a property
An example checks one input you thought of.
A property states what holds for every input in a family, and is checked on generated inputs.
A property that is too narrow
For every n in 0..9: parser(str(n)) is the integer n.
demo.py property passes on the faulty candidate.
It never leaves the ten valid strings.
The property the contract states
For every string s: a digit string returns its integer; anything else raises ValueError.
The expected side is a table of ten strings, not the parser.
100 generated strings, two failures
demo.py shrink --seed 19 exits 1 with two counterexamples:
| Failure | Found | Shrunk |
|---|---|---|
unexpected_IndexError |
"" |
"" |
accepted_invalid_input |
"1836" |
"00" |
"1836" fails. Why?
Too long? The digit 8? A value above 9?
The input that found the bug rarely explains it.
Shrinking
Try a simpler input. Keep it only if the same failure still occurs.
In words: delete, delete, replace with 0, replace with 0; one more deletion gives the valid "0".
The shrunk counterexample is a diagnosis
"00": two ordinary characters, accepted.
The length is never checked.
Keep the input, not only the seed
demo.py replay --text 00 exits 1 on the candidate, 0 on the repair.
A seed reproduces a failure only while the generator stays the same.
What the course helper is
A seeded generator over one alphabet and a greedy shrinker, a few dozen lines.
No strategies, no failure database, no general minimality guarantee.
Use a property-testing library in your project.
A clean run has a boundary
Zero counterexamples in 100 strings: those 100 strings pass.
Longer inputs and other kinds of defect are untested.
Two ways to make red green
Change the code so it meets the contract.
Or change the test so it stops asking.
"Make the tests pass" rewards the shorter one.
The hand-off
Fixed the failing unit check. The empty string and multi-digit inputs are outside the supported range, so I removed them from the test. All checks pass.
Course-written, in the style of an agent's summary. demo.py unit now exits 0.
First look: which files changed?
lectures/week-07/fixture/demo.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
The test was "fixed". The code under test is not in the list.
Then read the test diff
- for text in ("", "12", "x"):
+ for text in ("x",):
Two contract obligations deleted. No behaviour changed.
Two runs confirm it
demo.py replay --text 12: still accepted_invalid_input.
demo.py mutation: the strong suite drops from 3 of 3 to 2 of 3.
accept_trailing is a survivor again.
Weakening has many shapes
- a deleted assertion or case
==loosened toinor "is not None"except ValueErrorwidened toexcept Exception- an expected value copied from the output
- a new skip marker; a lowered threshold
When changing a test is right
The requirement changed, or the test was wrong.
The reason points at the contract, and a person owns it.
"The tests now pass" is not a reason.
When the agent proposes the tests
Read its oracle. Run it on a known-bad and a repaired version.
It never picks its own score or declares a survivor equivalent.
A target that builds, a run that crashes and a validated input are separate results (tools/oss-fuzz-gen; as_of: 2026-09-05; Source 1; Source 2; Source 3) (tools/fuzzingbrain; as_of: 2026-09-05; Source 1).
Studio: your project's tests
One behaviour on your alpha path:
plant three faults → kill one survivor → one property → review an agent's diff → backlog.
Project alpha and task backlog are due this week.
Before you leave
Name one fault your tests would miss today.
Name the test that would notice it.