This page is the hands-on drill for Mutation testing . The parent note built the vocabulary (mutant, killed, survived, equivalent) and the score formula. Here we exhaust every kind of situation a mutation-testing question can throw at you and work each one from zero.
Intuition What "every scenario" means here
A mutation-testing problem always reduces to one of a few shapes: does this mutant survive or die? , what is the score? , is this mutant equivalent? , plus the degenerate edges (no asserts, zero mutants, a test that catches every mutant by luck). We will hit each cell below at least once.
Before any example, here is the full space of cases. Each worked example is tagged with the cell it fills.
Cell
Case class
The tricky part
C1
Killed mutant (obvious)
Mutant changes output on an existing test input
C2
Survived mutant (missing boundary)
The differing input value is never tested
C3
Boundary fix
Add the exact value where operators diverge
C4
Equivalent mutant
Syntactically changed, behaviour identical → uncountable
C5
Score with equivalents
Must remove E from the denominator
C6
Degenerate suite (no asserts)
100% coverage, score = 0
C7
Zero / limiting inputs
Score when K = 0 , K = M − E , or M = E (division by zero)
C8
Real-world word problem
Translate a business rule into kill/survive reasoning
C9
Exam twist (two operators, one line)
Pick which mutant survives and why
def sign (n):
if n < 0 : return "neg"
return "nonneg"
Tests: assert sign(-4) == "neg" and assert sign(7) == "nonneg".
Mutant: n < 0 → n > 0. Killed or survived?
Forecast: Guess now — does flipping < to > change what the existing tests see?
Step 1 — run the mutant on sign(-4).
Why this step? A mutant is killed the instant one existing input produces a different output. Start with the input most likely to differ.
Original: -4 < 0 is True → "neg". Mutant: -4 > 0 is False → falls through → "nonneg".
Step 2 — compare to the expected value.
Why this step? The test asserts "neg"; the mutant returns "nonneg".
"nonneg" != "neg" → the assertion fails .
Step 3 — conclude.
Why this step? One failing test is the definition of a killed mutant.
KILLED. ✅
Verify: Sanity check with the other test — sign(7): original 7<0=False→"nonneg"; mutant 7>0=True→"neg". That test also fails. We only needed one, but both catching it confirms the suite is alert here.
Cell filled: C1.
def can_vote (age):
return age >= 18
Tests: assert can_vote(25) == True and assert can_vote(10) == False.
Mutant: age >= 18 → age > 18. Survive or killed?
Forecast: >= and > disagree at exactly one value. Which one? Is it in the tests?
Step 1 — find where the two operators disagree.
Why this step? >= and > give the same answer everywhere except one point : at a g e = 18 , >= is True but > is False. That single value is the only thing that can distinguish them — see the number line.
Step 2 — check whether any test uses that value.
Why this step? A mutant can only die if a test feeds it the distinguishing input . Our inputs are 25 and 10 — neither is 18 .
At 25 : both operators give True. At 10 : both give False. No difference visible.
Step 3 — conclude.
Why this step? All tests pass on the mutant ⇒ by definition it SURVIVED. 😱 (Cell C2.)
Step 4 — the fix (Cell C3).
Why this step? A survivor is an instruction to strengthen the tests (parent note, Mistake 2). Add the boundary value — this is exactly Boundary value analysis .
Add assert can_vote(18) == True. Now the mutant returns 18 > 18 = False, expected True → fails → KILLED. ✅
Verify: After the fix, re-run on the mutant: input 18 gives False, assertion wants True → mismatch. Killed confirmed. The other two tests still pass (they always did), so we broke nothing.
Cells filled: C2, C3.
def scale (x):
total = 0
for i in range (x):
total = total + 1
return total
Mutant: change the loop header range(x) → range(0, x). Killed, survived, or equivalent?
Forecast: Does range(x) ever produce a different sequence from range(0, x)?
Step 1 — compare the two expressions' behaviour, not their text.
Why this step? The test for equivalence is behavioural : does any input ever make the outputs differ? In Python, range(x) is defined as range(0, x). Same sequence for every x.
Step 2 — argue no input can distinguish them.
Why this step? For a mutant to be killable, some input must change some output. Here the return value is identical for all x (including x = 0 , giving 0; negatives give an empty range in both). No test can ever fail on this mutant.
Step 3 — classify.
Why this step? "Syntactically different, behaviourally identical" is the exact definition of an equivalent mutant . It contributes to E , and we exclude it from the denominator.
EQUIVALENT. It should be counted in E , never as a survivor-to-fix.
Verify: Enumerate a few inputs: x = 0 ⇒ 0 = 0 ; x = 3 ⇒ 3 = 3 ; x = − 2 ⇒ 0 = 0 . Outputs match everywhere → equivalence holds.
Common mistake Don't chase this survivor
If a tool reports this mutant as "survived", the wrong move is to add a test. There is no test that kills it. The right move is to mark it equivalent and bump E .
Cell filled: C4.
A tool generates M = 80 mutants. Manual review finds E = 5 equivalent. Tests kill K = 60 . What is the mutation score, and how many killable mutants survived?
Forecast: Is the denominator 80 or 75? Which gives the honest score?
Step 1 — compute the killable population.
Why this step? Equivalent mutants can never die, so penalising the suite for them is unfair (parent note, Step 1). Killable = M − E = 80 − 5 = 75 .
Step 2 — apply the formula.
Why this step? Score is "killed over killable".
Score = M − E K = 80 − 5 60 = 75 60 = 0.8 = 80%
Step 3 — count the real survivors.
Why this step? Survivors that matter are killable-but-not-killed = ( M − E ) − K = 75 − 60 = 15 . These 15 are your to-do list of missing tests.
Verify: Sanity: killed + real-survivors = 60 + 15 = 75 = M − E ✅. If we had wrongly used M = 80 : 60/80 = 75% — lower, understating the suite. Removing E correctly raises it to 80% .
Cell filled: C5.
def discount (price):
if price > 100 : return price * 0.9
return price
"Test": discount(150); discount(50) — it calls the function but has no assert . A tool makes 4 mutants, all non-equivalent (E = 0 ). What is the score?
Forecast: Coverage is 100% (both branches run). Guess the mutation score before reading.
Step 1 — recall what kills a mutant.
Why this step? A mutant dies only when a test fails . A test with no assertion cannot fail (barring a crash) — it just runs.
Step 2 — evaluate each mutant.
Why this step? Change > 100 → >= 100, or * 0.9 → * 0.1, etc. Every mutant still runs to completion; with no assert, nothing raises. All survive .
Killed K = 0 .
Step 3 — apply the formula.
Why this step? E = 0 , so denominator is M = 4 .
Score = 4 − 0 0 = 0 = 0%
Verify: Coverage would report 100% here (both branches executed). Mutation score reports 0% . This is the exact gap the parent note's first mistake warns about — coverage measures execution , mutation measures assertion strength .
Cell filled: C6.
For the same M = 80 , E = 5 setup, evaluate the score at three limits: (a) K = 0 , (b) K = 75 (all killable killed), (c) the degenerate case M = E (every mutant equivalent).
Forecast: One of these makes the formula undefined . Which?
Step 1 — case (a) K = 0 .
Why this step? Lower bound. 75 0 = 0 . A completely blind suite.
Step 2 — case (b) K = M − E = 75 .
Why this step? Upper bound. 75 75 = 1 = 100% . Every killable mutant died — the strongest possible score. Note it is 100% despite 5 equivalents surviving; those are correctly excluded.
Step 3 — case (c) M = E (say M = E = 5 , so K = 0 necessarily).
Why this step? This is the degenerate / division-by-zero edge. Denominator M − E = 0 .
Score = 0 0 — undefined.
When every mutant is equivalent, there is nothing killable , so the score is not just 0 but undefined — the question "what fraction did you catch?" has no meaning when there was nothing to catch. Tools report this as N/A.
Verify: (a) 0/75 = 0 ✅ (b) 75/75 = 1 ✅ (c) denominator = 0 , so the ratio is undefined — a tool must guard against it, never print 0% .
Cell filled: C7 (all three limits).
An online store gives free shipping when the cart total is at least ₹500 . A developer writes:
def free_shipping (total): return total >= 500
with tests free_shipping(700) == True and free_shipping(300) == False. The CI (Continuous integration ) mutation run flips >= → >. The business asks: "Will this survive, and does it matter for real customers?"
Forecast: Which real customer is affected by the >= vs > difference?
Step 1 — locate the distinguishing customer.
Why this step? >= and > differ only at total == 500 — a customer with exactly ₹500 in the cart.
Step 2 — check the tests.
Why this step? Inputs are ₹700 and ₹300; neither is ₹500. So both operators agree on both tests → mutant SURVIVES.
Step 3 — translate to business impact.
Why this step? The mutant total > 500 would deny free shipping to a customer spending exactly ₹500 — a real, angry-customer bug that the suite would not catch.
Add assert free_shipping(500) == True. Mutant now returns 500 > 500 = False ≠ True → KILLED.
Verify: Post-fix, feed ₹500 to the mutant: False, expected True → fails → killed ✅. The exact-threshold customer is now protected. This is why boundary tests (Boundary value analysis ) and mutation testing pair so well.
Cell filled: C8.
def grade (score):
if score >= 50 and score <= 100 :
return "pass"
return "fail"
Test: assert grade(75) == "pass" and assert grade(40) == "fail".
Two mutants are proposed. Mutant A: and → or. Mutant B: score <= 100 → score < 100. For each: killed or survived?
Forecast: One of these dies on the existing tests, the other slips through. Which is which?
Step 1 — Mutant A (and → or).
Why this step? or changes the logic broadly, so look for any input that flips. Try grade(40): original needs both 40>=50 (False) → "fail". Mutant: 40>=50 OR 40<=100 → False or True → True → "pass". Expected "fail" → fails → KILLED. ✅
Step 2 — Mutant B (<= 100 → < 100).
Why this step? <= and < differ only at score == 100. Our tests use 75 and 40 — neither is 100.
grade(75): both give True and True → "pass" ✅ (agrees). grade(40): fails the first clause anyway → "fail" (agrees). No test hits 100 → SURVIVED. 😱
Step 3 — the twist explained.
Why this step? The exam trap is assuming "same line, so same fate". Different operators fail at different inputs. and→or is caught by a mid-range value; the boundary flip needs the exact boundary (100).
Fix for B: add assert grade(100) == "pass". Mutant returns 100 < 100 = False → "fail" ≠ "pass" → KILLED.
Verify: Mutant A on input 40: (False) or (True) = True → "pass" ≠ "fail" → killed ✅. Mutant B on input 100 post-fix: first clause 100>=50=True, second 100<100=False, True and False=False → "fail" ≠ "pass" → killed ✅. Both now dead.
Cell filled: C9.
Recall Which cell was each example?
C1 obvious kill ::: sign with <→>
C2 + C3 survivor + boundary fix ::: can_vote >=→>, fixed by testing age 18
C4 equivalent ::: range(x)→range(0,x)
C5 score with equivalents ::: 60/ ( 80 − 5 ) = 80%
C6 no-assert suite ::: 100% coverage, 0% mutation score
C7 limits ::: 0 , 100% , and undefined (M = E )
C8 word problem ::: free-shipping at exactly ₹500
C9 exam twist ::: and→or dies, <=→< survives
Mnemonic The one question that solves every cell
"Where do the two versions disagree — and does any test go there?" If yes → killed . If the disagreement point is unreachable by any test → survived . If they never disagree → equivalent .
← Back to the parent topic
Boundary value analysis — cells C2, C3, C8, C9 are all boundary survivors.
Unit testing — every "add an assert" fix strengthens a unit test.
Code coverage — cell C6 is the coverage-vs-mutation gap made concrete.
Continuous integration — where the C8 nightly mutation run lives.
Test-driven development — writing the boundary test first pre-empts C2-style survivors.
Fault injection — the broader family these deliberate mutants belong to.