The Library App That Passed Every Test
A small team is building a library borrowing app. Each borrower may hold at most five loans, and a loan is rejected if the borrower has any overdue item. The team wrote unit tests for each function in isolation: canBorrow(borrower) returns false when the borrower has five loans, and hasOverdue(borrower) returns true when any loan is overdue. All 42 unit tests pass and coverage is 91%. In staging, a tester with four loans and one overdue book successfully borrowed a sixth title. The code path is if (canBorrow(b) && hasOverdue(b)) { reject } else { approve }. The team lead argues the bug is a tester error because the unit tests prove both functions correct, and proposes adding more unit tests for each function. The release is scheduled in three days.
1. Explain why the passing unit tests did not prevent this defect, and identify the specific line of reasoning that is faulty.
Read the worked answer
The unit tests confirm that each function behaves correctly in isolation; they say nothing about whether the functions are composed correctly. The defect is in the integration logic, not in either function.
Tracing the expression with the tester's data: canBorrow(b) is true (only four loans), and hasOverdue(b) is true (one overdue item). So true && hasOverdue(b) evaluates to true, the reject branch is taken - wait, in this trace the tester was approved. Re-reading the condition, the guard is written as if (canBorrow && hasOverdue) reject, which means a borrower is rejected only when they are eligible and overdue simultaneously. A borrower who is ineligible (five loans) but not overdue falls to else and is approved. That is the inversion: the rejection should trigger on !canBorrow(b) || hasOverdue(b).
The faulty reasoning is the inference "both units pass, therefore the system is correct." Unit tests cannot detect a wrongly wired condition because the condition itself is never exercised - it lives between the units. Coverage of 91% measures lines executed, not combinations of states tested. The lead's proposal to add more unit tests would raise coverage without touching the defect.
The correct response is an integration test that drives the composition with a table of borrower states: (eligible, not overdue) → approve; (eligible, overdue) → reject; (ineligible, not overdue) → reject; (ineligible, overdue) → reject. That four-row table exposes the inversion immediately. This is why test design should target decision boundaries and state combinations, not just function bodies.
2. The team has three days. Recommend a testing strategy for the remaining time and justify the trade-offs.
Read the worked answer
With three days, the priority is to stop shipping a wrong decision, then widen coverage where risk is highest.
Day 1 - fix and verify the composition. Correct the condition to !canBorrow(b) || hasOverdue(b). Add the four-row decision table as an integration test. Add boundary cases at exactly five loans and exactly zero overdue items, since off-by-one errors cluster at boundaries. These tests are cheap and directly target the known failure.
Day 2 - risk-based integration testing. List the user journeys that cross module boundaries: borrow, return, renew, and the interaction of renew with overdue status. Test each end-to-end in staging with realistic data. Do not chase coverage percentage; chase decision combinations. A branch that has never been false is a branch that has never been tested.
Day 3 - regression and release gate. Freeze features, run the full suite, and record which risks remain untested so the release decision is informed rather than assumed.
Trade-offs. The team cannot exhaustively test every state combination in three days, so it accepts residual risk in low-traffic paths (for example, renewing a title that is simultaneously reserved by another borrower). It also accepts that coverage will dip slightly as new integration tests are added, because coverage is a proxy, not a goal.
The judgement being tested is that more of the same test type is not more assurance. The lead's instinct - add unit tests - would consume the three days and leave the defect class untouched.