Skip to content
Preptima

QA and testing fundamentals

What a test actually buys you, why the pyramid is argued about, the five test doubles and when each is right, what makes a suite flaky, and how non-functional testing is read. Fifty-nine items, fifteen worked through with code, a result table or a diagram.

59 questions

Go deeper on QA & Testing
Standard Sheet View

What tests are for

What does a test suite actually buy you?

The ability to change the code. A system without tests is not one that has more bugs — it may have very few — it is one where every modification carries unknown risk, so the rational response is to stop modifying it, and that is how a codebase becomes something nobody will touch. Tests convert "I believe this still works" into "the machine checked", which is what makes refactoring, dependency upgrades and rewrites of internals affordable. That framing also tells you which tests are worth writing: the ones that will still pass after a change you intend to make, and fail after a change you did not intend.

Why is "tests find bugs" the wrong headline?

Because tests mostly do not find bugs; they find regressions. A test written after the code passes on the first run almost by definition, and the defects that matter were usually never imagined by the person writing the assertions. What the suite does is fix the behaviour in place so that tomorrow's change cannot quietly alter it. Saying this out loud matters in an interview, because it leads to different decisions: you invest in tests around behaviour that is expensive to break, rather than chasing a percentage over code that nobody will ever edit again.

Show me the red/green/refactor loop and what each step is for.

Each step exists to check something the other two cannot, which is why skipping one quietly removes the benefit.

The red step is the only point at which you verify the test itself. A test that has never failed is an unverified instrument: it may assert on the wrong object, be silently skipped, or exercise a code path that was already correct. Watching it fail for the reason you expected is the whole reason the step is first.

The green step is deliberately ungenerous. Writing the smallest thing that passes keeps you from building machinery no test demands, and it exposes the next test you need, because the obviously-inadequate implementation tells you which case is still unspecified.

Refactor is the step teams drop, and dropping it converts TDD into a slow way of writing badly structured code with tests attached. The suite exists precisely so that this step is safe; not using it wastes the thing you paid for.

The dotted branch is the common failure in practice. If your new test is green first time, either the behaviour already existed — delete the test or keep it as documentation, knowingly — or the test is not wired to the code you think it is.

What is TDD's actual claim?

Not that it finds more bugs, which the evidence is equivocal about. The claim is about design: writing the test first forces you to use the interface before it exists, so awkward construction, hidden dependencies and untestable coupling show up as pain while they are still cheap to change. A class that needs six mocks to instantiate is telling you something about the class, and TDD makes you hear it in the first minute rather than the third week. The secondary claim is behavioural — it produces small steps and a working system at all times, which keeps debugging sessions short because the last change was tiny.

What does code coverage tell you?

Which lines were executed while the tests ran. That is genuinely useful as a negative signal: a module at five per cent has no safety net, and a diff that adds forty uncovered lines is worth a comment in review. What coverage cannot tell you is whether anything was checked, because a test with no assertions covers every line it touches. So it is a floor detector, not a quality measure, and the moment it becomes a target it is gamed — by tests that call code and assert nothing, by excluding awkward files, and by testing getters to raise the average.

Show me a coverage report at full marks with the bug still alive.

One function, one test, every line green, and a defect that reaches production.

int discountedPrice(int priceMinor, int percentOff) {
    if (percentOff > 100) percentOff = 100;      // covered
    return priceMinor - (priceMinor * percentOff / 100);   // covered
}

@Test
void appliesDiscount() {
    assertEquals(9000, discountedPrice(10000, 10));
}
lines   6/6    100%
branch  2/2    100%       <- the if was taken and not taken across the suite

The report is honest and useless. The test exercised both lines and one branch outcome, and the suite happens to cover the other elsewhere, so the tool has nothing left to complain about. Meanwhile percentOff = -50 returns a price higher than the input, and nothing in the code or the suite has an opinion about negative discounts.

The general shape of the gap: coverage measures execution, and correctness lives in the values. Line coverage cannot see the untested input, branch coverage cannot see the missing branch, and no coverage metric can see the assertion you did not write.

Two things do see it. Thinking in equivalence classes and boundaries — below zero, zero, one, ninety-nine, one hundred, above one hundred — would have produced the failing case in seconds. And mutation testing would have flipped the guard to < 100 or deleted it entirely, found the suite still green, and reported the line as covered but unprotected.

That is the distinction worth carrying into an interview: covered means visited, protected means a change here breaks a test. Only the second is what anyone actually wanted.

What is mutation testing, and what does it fix?

It deliberately introduces small faults into your code — flipping a comparison, negating a condition, replacing a return with a constant — and runs the suite against each mutant. A mutant the tests still pass is called survived, and it names a change to your code that no test would catch. That is the measurement coverage cannot give: not which lines ran, but which lines are actually defended. The cost is runtime, because it is your whole suite multiplied by the number of mutants, so it belongs on a nightly or weekly schedule over the modules where correctness matters rather than in the pull-request gate.