Testing
JUnit 5, Arrange-Act-Assert, the TDD cycle, Mockito test doubles, flaky tests, mutation testing, contract testing, and integration tests.
8 questions
JuniorTheoryVery commonWhat does the test-structuring convention Arrange-Act-Assert give a unit test?
What does the test-structuring convention Arrange-Act-Assert give a unit test?
It splits the test body into three blocks: Arrange builds the fixture and inputs, Act invokes the one behaviour under test, and Assert checks the outcome. Keeping a single Act per test means a failure points at exactly one behaviour, and the fixed shape lets any reader scan an unfamiliar test in seconds. It is a convention, not something JUnit enforces.
Common mistakes
- ✗Packing several Act calls into one test, so a failure no longer names one behaviour
- ✗Believing JUnit enforces the three blocks rather than it being a convention
- ✗Treating assertions scattered through the setup as the Assert block
Follow-up questions
- →Why should a unit test keep a single Act step?
- →How does the behaviour-driven wording Given-When-Then map onto Arrange-Act-Assert?
JuniorTheoryCommonIn the testing framework JUnit 5, what do @Test, @ParameterizedTest and @RepeatedTest do?
In the testing framework JUnit 5, what do @Test, @ParameterizedTest and @RepeatedTest do?
@Test marks one no-argument test method. @ParameterizedTest runs the same method once per input taken from a declared source such as @ValueSource or @CsvSource. @RepeatedTest(n) runs the method n times. JUnit 5 also builds a fresh test-class instance for every test, so tests cannot leak state into one another.
Common mistakes
- ✗Thinking
@RepeatedTestretries a failing test until it passes - ✗Expecting a
@ParameterizedTestto run without any declared argument source - ✗Assuming one test-class instance is shared across all of its tests
Follow-up questions
- →Which argument sources can feed a
@ParameterizedTest? - →How do
@BeforeEachand@BeforeAlldiffer in the test lifecycle?
JuniorTheoryCommonIn test-driven development, what are the three steps of the red-green-refactor cycle?
In test-driven development, what are the three steps of the red-green-refactor cycle?
Red: write a failing test for behaviour that does not exist yet, and watch it fail. Green: write the simplest code that makes it pass, however crude. Refactor: clean up code and test while the suite stays green, so the tests act as a safety net. Then repeat in small increments — the test is written first, and it drives the design.
Common mistakes
- ✗Writing the production code first and the test afterwards
- ✗Skipping the refactor step once the test has turned green
- ✗Confusing TDD with a coverage target rather than a design cycle
Follow-up questions
- →Why must you see a new test fail before making it pass?
- →How does writing the test first shape a class's API, not just its coverage?
MiddleDebuggingCommonA JUnit 5 test class passes locally but fails intermittently in CI — diagnose it
A JUnit 5 test class passes locally but fails intermittently in CI — diagnose it
The tests share mutable state through the static list and depend on execution order, which JUnit 5 does not guarantee. When createsOrder runs first, ORDERS.get(0) is today's order, which is not expired — so the assertion fails. Run alone or in the other order, it passes. Fix by making the fixture per-test (a non-static field, reset in @BeforeEach) and asserting on the order the test itself created, not on index 0.
Common mistakes
- ✗Assuming JUnit runs test methods in declaration order
- ✗Calling a retry rule a fix — it hides the shared state instead of removing it
- ✗Sharing a fixture through a
staticfield to 'save time' in setup
Follow-up questions
- →Why is a wall-clock read such a common source of flakiness, and how do you remove it?
- →Why does
@TestMethodOrdermake the suite pass without making it correct?
MiddleTheoryCommonIn the mocking library Mockito, how do a mock, a stub and a spy differ?
In the mocking library Mockito, how do a mock, a stub and a spy differ?
A mock is a generated fake object: every method returns a default (null, 0, false) until you say otherwise, and its calls can be checked with verify. Stubbing is what you do to a double — when(x.f()).thenReturn(v) pins a canned answer on one call. A spy wraps a real instance: unstubbed methods run the real implementation, only the stubbed ones are replaced. So mock and spy are the objects; stubbing is the behaviour you attach.
Common mistakes
- ✗Expecting a mock's unstubbed method to run the real implementation
- ✗Expecting a spy's unstubbed method to return a default instead of the real result
- ✗Using
when(spy.f())on a spy, which calls the real method —doReturn(...).when(spy).f()avoids it
Follow-up questions
- →Why does
when(spy.method())call the real method, whiledoReturn(...).when(spy).method()does not? - →When is a hand-written fake preferable to a Mockito mock?
MiddleDesignOccasionalLast Friday the orders team renamed a JSON field from customerId to customer_id in their REST response. Their own tests were green, they deployed on schedule, and two downstream services — billing and notifications — started throwing null-pointer failures in production within minutes. Six teams own these services, each deploys several times a day, there is no shared staging environment, and the end-to-end suite that would have caught it takes 40 minutes and is itself flaky enough that people re-run it until it goes green. Management now wants a rule that no producer can ship a change that breaks a consumer. Design the testing approach you would introduce: what exactly gets asserted, who writes it, where it runs in each team's pipeline, and how it fails a producer's build before the deploy rather than after it.
Last Friday the orders team renamed a JSON field from customerId to customer_id in their REST response. Their own tests were green, they deployed on schedule, and two downstream services — billing and notifications — started throwing null-pointer failures in production within minutes. Six teams own these services, each deploys several times a day, there is no shared staging environment, and the end-to-end suite that would have caught it takes 40 minutes and is itself flaky enough that people re-run it until it goes green. Management now wants a rule that no producer can ship a change that breaks a consumer. Design the testing approach you would introduce: what exactly gets asserted, who writes it, where it runs in each team's pipeline, and how it fails a producer's build before the deploy rather than after it.
Introduce consumer-driven contract tests. Each consumer writes, against a local stub of the producer, the requests it makes and the response fields it actually reads — that expectation is the contract, published to a broker. The producer's build then replays every published contract against its real API and fails when a field a consumer reads has moved or vanished, so the rename would have failed the orders build before the deploy. No shared environment is needed.
Common mistakes
- ✗Letting the producer author the contract, so it asserts what the producer ships rather than what a consumer reads
- ✗Treating an OpenAPI schema as a contract test — nothing fails when a consumer's field disappears
- ✗Trying to buy compatibility with a bigger end-to-end suite, which is slow, flaky and finds the break after the deploy
Follow-up questions
- →Why must the contract capture only the fields a consumer reads, not the producer's whole response?
- →How does a producer safely add a field, and why is that not a contract break?
MiddleTheoryOccasionalWhat does mutation testing measure that line coverage cannot?
What does mutation testing measure that line coverage cannot?
A tool such as PIT makes small changes to the compiled production code — flips > to >=, replaces a return value, deletes a call — and re-runs the suite against each mutant. A mutant that makes some test fail is killed; one the green suite never notices survives, and the score is killed/total. It therefore measures whether your assertions detect wrong behaviour, while line coverage only proves the line ran — a test with no assertions still covers 100%.
Common mistakes
- ✗Treating 100% line coverage as proof the tests would catch a regression
- ✗Thinking the tool changes the tests rather than the production code
- ✗Reading a surviving mutant as a bug in production code rather than a gap in the assertions
Follow-up questions
- →Why can a suite hit 100% line coverage and still kill almost no mutants?
- →What is an equivalent mutant, and why does it cap the achievable score?
MiddleDesignOccasionalYou are reviewing the test suite of a checkout service before release. Every one of its 400 tests is annotated @SpringBootTest: each starts the full application context, a real PostgreSQL container and the HTTP layer, and the suite now takes 38 minutes — so developers push without running it and CI is the first place a break is noticed. Reading the tests, most of them assert pure logic: discount arithmetic, cart-total rounding, coupon-expiry rules. A minority genuinely depend on infrastructure: JPA mappings, @Transactional rollback behaviour, and the JSON contract of the REST controller. The team wants PR feedback under five minutes without losing the bugs the slow tests actually catch. Which tests would you rewrite as plain Mockito unit tests and which must stay integration tests, what does each layer buy you, and how would you keep the payment provider out of both?
You are reviewing the test suite of a checkout service before release. Every one of its 400 tests is annotated @SpringBootTest: each starts the full application context, a real PostgreSQL container and the HTTP layer, and the suite now takes 38 minutes — so developers push without running it and CI is the first place a break is noticed. Reading the tests, most of them assert pure logic: discount arithmetic, cart-total rounding, coupon-expiry rules. A minority genuinely depend on infrastructure: JPA mappings, @Transactional rollback behaviour, and the JSON contract of the REST controller. The team wants PR feedback under five minutes without losing the bugs the slow tests actually catch. Which tests would you rewrite as plain Mockito unit tests and which must stay integration tests, what does each layer buy you, and how would you keep the payment provider out of both?
Split by what a test actually needs. Pure logic — discounts, rounding, coupon rules — has no reason to boot Spring: instantiate the class, mock its collaborators, run in milliseconds. That is the bulk of the 400. Keep an integration test only where the thing under test is the wiring: JPA mappings, transaction rollback, the controller's JSON contract — a mocked repository can never fail a mapping. The payment provider stays out of both, replaced at its boundary.
Common mistakes
- ✗Reaching for
@SpringBootTestby default, so every test pays for a context it does not use - ✗Mocking the
EntityManagerand believing the JPA mapping is then covered - ✗Letting an integration test call the real payment provider, which makes the suite depend on a third party
Follow-up questions
- →Why can a mocked repository never catch a broken
@OneToManymapping? - →When is a sliced test (
@DataJpaTest,@WebMvcTest) the right middle ground?