4.5.16 · D4Software Engineering

Exercises — Mutation testing

3,501 words16 min readBack to topic

This is the practice arena for Mutation testing (4.5.16). Every exercise is graded L1 → L5. Read the problem, try it yourself first, then open the solution. Every number here is machine-verified.

Before we start, one quick reminder of the counts everything below leans on. We use exactly four. The picture below is the mental model for all of them — keep it in mind for every exercise:

Figure — Mutation testing

Level 1 — Recognition

Goal: name the pieces. No calculation harder than counting.

L1.1

Problem. Match each event to the right word AND its symbol from the four counts: (a) a test failed on the mutated program; (b) every test passed on the mutated program; (c) the mutant does the exact same thing as the original.

Recall Solution
  • (a) → Killed mutant — the suite noticed the injected bug. Counted in (blue box in the figure).
  • (b) → Survived mutant — the suite missed it (a gap in the tests, not necessarily a bug in production code). Counted in (pink box).
  • (c) → Equivalent mutant — no test can ever kill it, so we exclude it from the denominator as (yellow box). It is one kind of survivor, so it also sits inside — that is why it's drawn nested against the pink region. Remember the intuition: it's unkillable by definition, so it can't be an opportunity.

L1.2

Problem. A tool changes x + y into x - y. What is the name of the rule that made this change, and what is the resulting program called?

Recall Solution
  • The rule is a mutation operator (here, the arithmetic-swap operator +-).
  • The resulting program (with that one change) is a mutant.

L1.3

Problem. A test method runs the line return age >= 18 but contains no assert at all. What is its line coverage on that line, and how many mutants of that line can it kill?

Recall Solution
  • Line coverage: 100% — the line executed.
  • Mutants killed: 0 — with no assertion, nothing can ever fail, so no mutant is caught. This is exactly the gap Code coverage hides and mutation testing exposes.

Level 2 — Application

Goal: plug numbers into , and hand-simulate one mutant.

L2.1

Problem. A run gives mutants, equivalent, killed. Compute the mutation score as a percentage (1 decimal place).

Recall Solution

Killable population (and , so the formula is valid). Why carve out the 5? Those 5 are unkillable by definition — leaving them in would pretend the suite could have caught them.

L2.2

Problem. Same run as L2.1, but a colleague wrongly forgets to subtract the equivalents and computes . What (wrong) number do they get, and by how many percentage points does it under-report the true score?

Recall Solution

Wrong value: . True value (L2.1): . Under-report gap percentage points. Equivalent mutants can only be killed by nobody — that's the intuition again — so including them in the denominator drags the score down for a failure that was never the suite's fault.

L2.3

Problem. Hand-simulate. Code and test:

def sign(n):
    if n >= 0:
        return "nonneg"
    return "neg"
assert sign(4)  == "nonneg"
assert sign(-2) == "neg"

Mutant: change n >= 0n > 0. Does it survive or get killed?

Recall Solution

Look at the two tests against the mutant n > 0 — and see the number-line figure below, where >= and > only disagree at a single boundary point:

  • sign(4): 4 > 0 is true → returns "nonneg" ✓ (matches).
  • sign(-2): -2 > 0 is false → returns "neg" ✓ (matches).

Both tests pass → mutant SURVIVES (goes into ). The only input where >= and > differ is n = 0, and no test uses it. This is a boundary gap. Adding assert sign(0) == "nonneg" kills it, because the mutant returns "neg" for 0. Since a distinguishing input exists, this survivor is killable, not equivalent — so it stays in the denominator.

Figure — Mutation testing

Level 3 — Analysis

Goal: reason about WHY a mutant survives, and design the test that kills it.

L3.1

Problem. Code: def in_range(x): return 1 <= x <= 10. Test suite: assert in_range(5) == True, assert in_range(50) == False. For each mutant below, decide survive/killed with the given tests, and if it survives give ONE input that kills it.

  • (i) 1 <= x1 < x
  • (ii) x <= 10x < 10
Recall Solution

Tests only probe (inside) and (far outside). Neither touches a boundary — look at the two boundary dots ( pink, yellow) in the number-line figure above.

  • (i) 1 < x. in_range(5): 1<5 and 5<=10 → True ✓. in_range(50) → False ✓. Both pass → SURVIVES. Killer input: x = 1. Original: 1<=1 → True; mutant: 1<1 → False. Test assert in_range(1) == True fails on mutant → killed.
  • (ii) x < 10. in_range(5) → True ✓, in_range(50) → False ✓. SURVIVES. Killer input: x = 10. Original: 10<=10 → True; mutant: 10<10 → False. assert in_range(10) == True kills it.

Because each survivor has a distinguishing input (found by testing the boundary), both are killable survivors, not equivalents — they belong in the denominator.

L3.2

Problem. Code: def f(a): return a * 1. A tool mutates a * 1a + 0. The mutant survives every possible test. Classify this mutant and explain what a survivor of this type does NOT tell you.

Recall Solution

a * 1 and a + 0 both equal a for every value of a — this is exactly the trivial-algebra case the detection checklist flags automatically. The programs are behaviourally identical, so no input can ever distinguish them → this is an equivalent mutant. What it does not tell you: it does not indicate a weak test suite. An equivalent survivor is a false positive — it counts toward (excluded from the denominator, the yellow box), not toward "add a test." Here is the intuition made concrete: there is literally no killer input, so it was never an opportunity to begin with.

L3.3

Problem. You have 30 mutants. 18 are killed by your current tests. Of the 12 survivors, careful analysis shows 3 are equivalent and 9 are genuinely killable but currently missed. (a) Current score? (b) If you add tests that kill all 9 real survivors, new score? (Both as fractions and percentages.)

Recall Solution

Here , (of which ), . Killable . We carve out the 3 equivalents because — one more time — they can never be killed by anyone.

  • (a) Now: . .
  • (b) After killing the 9 killable survivors: . . Every killable mutant is now dead — and notice the 3 equivalents were never in the denominator, which is exactly why 100% is reachable here.

Level 4 — Synthesis

Goal: combine concepts — build the killing test, connect to other techniques.

L4.1

Problem. Code: def discount(price, member): return price * 0.9 if member else price. Your only test is assert discount(100, True) == 90. Write the minimum set of additional assertions that would kill each of these three mutants, and name each killing input.

  • (i) membernot member
  • (ii) price * 0.9price * 1.0
  • (iii) else priceelse price * 0.9
Recall Solution
  • (i) not member: with the existing test discount(100, True), mutant takes the else-branch → returns 100 ≠ 90already killed. No new test needed.
  • (ii) price * 1.0: discount(100, True) mutant returns 100 ≠ 90already killed by the existing test.
  • (iii) else price * 0.9: existing test only uses member=True, never the else-branch, so it can't see this. Need a non-member test. Killer: assert discount(100, False) == 100. Original returns 100; mutant returns 90 → fails → killed.

Minimum addition: exactly one assertion, assert discount(100, False) == 100, closing the untested else-branch. This is precisely how mutation testing grades your Unit testing and complements Test-driven development.

L4.2

Problem. A team runs 500 mutants nightly in Continuous integration. Night 1: , killed . Night 2 (after adding tests): same 500 mutants, , killed . (a) Compute both nights' scores (1 dp). (b) How many additional killable survivors did the new tests eliminate?

Recall Solution

Killable (unchanged — same code, same operators, so the same 20 equivalents are carved out both nights).

  • Night 1: .
  • Night 2: .
  • (b) Extra killed survivors newly caught by the added tests.

Level 5 — Mastery

Goal: full reasoning under trade-offs, and the 80/20 judgement call.

L5.1

Problem. Two suites are tested against the same 200 mutants (with ).

  • Suite A: kills 180, but takes 2 minutes to run the full mutation analysis.
  • Suite B: kills 171, and takes 40 seconds.

(a) Compute both scores (1 dp). (b) Suite A's extra 9 kills — expressed as percentage points of score — cost an extra 80 seconds. Comment on the 80/20 trade-off.

Recall Solution

Killable (the 10 equivalents are carved out for both suites, since they run against the same mutants).

  • Suite A: .
  • Suite B: .
  • (b) The gap is percentage points for seconds of runtime. Whether that's worth it depends on what those 9 mutants touch: if they sit on critical logic (payment, auth), the 4.7 points are cheap insurance; if they're low-risk edge cases, Suite B's speed may win, especially in a fast CI loop. Chasing the last few percent hits diminishing returns — the 80/20 principle from the parent note.

L5.2

Problem. A tool reports a mutation score of , treating all 3 survivors as killable. Later, review finds 2 of the 3 survivors are equivalent and 1 is a real gap. (a) Recompute the correct score. (b) You then add one test that kills the last real survivor. New correct score? (c) Explain in one sentence why the corrected score in (a) is higher than the reported 94%.

Recall Solution

Here the tool reported , "survivors" , and used denominator (it assumed ).

  • (a) Truly , so killable , and : .
  • (b) Killing the 1 real survivor: , killable .
  • (c) Removing the 2 unkillable equivalents shrinks the denominator (48 instead of 50) while stays 47, so the same successes are now measured against a fairer, smaller pool — the ratio rises. This is the intuition once more: equivalents were never opportunities, so counting them was quietly deflating the score.

L5.3

Problem. A CI pipeline enforces a quality gate: builds fail if mutation score . A run produces , . What is the minimum number of mutants the suite must kill to pass the gate? (Give the integer.)

Recall Solution

Killable . Require . Since must be a whole number of mutants, round up: . Check: (fails), (passes). So 96 kills are needed. (Note: only killable mutants count in the gate — the 8 equivalents sit outside the denominator, unkillable by definition.)


How these exercises hang together

The path is one straight climb, and each level feeds the next:

  • L1 Recognition — name the pieces ().
  • L2 Application — plug them into and hand-simulate one mutant.
  • L3 Analysis — reason about why a mutant survives (boundaries) and whether it is killable or equivalent.
  • L4 Synthesis — combine ideas: reuse one test to kill several mutants; connect to Unit testing and Test-driven development.
  • L5 Mastery — weigh trade-offs (runtime vs score, Continuous integration gates, the N/A edge case).

One intuition runs underneath the whole ladder and we re-earned it at every level: equivalent mutants are unkillable by definition, so they leave the denominator. Two prerequisite threads support the climb: Boundary value analysis explains almost every survivor you met (the > vs >= gaps), and detecting equivalent mutants is the hard, sometimes-undecidable quadrant handled by the checklist near the top.

Connections

  • Mutation testing — the parent topic these exercises drill.
  • Unit testing — the tests being graded in L4.1.
  • Code coverage — the weaker metric exposed in L1.3.
  • Boundary value analysis — why L2.3 and L3.1 survivors appear.
  • Test-driven development — the discipline mutation testing audits.
  • Continuous integration — where the L4.2 / L5.3 gates run.
  • Fault injection — the broader "break it on purpose" family.