JUnit 5, Mockito, and Spring testing — core concepts explained in depth.
Why cleaning up test data at the start beats the end, why in-memory databases still don't belong in integration tests despite Testcontainers changing the container-overhead trade-off, and why reads and repositories deserve a lower testing bar than writes.
Why schema and reference data belong in source control, why migration-based delivery beats state-based once real data exists (the data-motion argument), and why integration tests need a separate transaction per arrange/act/assert section instead of reusing one session.
Why constructor/@BeforeEach-based fixture reuse couples tests to each other and hurts readability, why private factory methods (Object Mother) are the better default, when to reach for @ParameterizedTest and @MethodSource, and how AssertJ's fluent style avoids the expected/actual argument-order trap.
The AAA pattern's real rules beyond "use three sections": never write multiple act sections or an if statement in a test, treat a multi-line act section as a production-code smell not a test problem, and reject rigid MethodUnderTest_Scenario_Result naming for plain-English test names.
Concrete, sometimes counter-intuitive rules for using mocks well beyond the mock-vs-stub distinction: mocks belong only in integration tests, never unit tests on pure domain logic; hand-written spies often beat a generic mock's verify(); assertions must check call count, not just occurrence; and you should only ever mock types your own codebase defines.
Why testing private methods, exposing private state, reimplementing the SUT's logic in a test, polluting production code with test-only switches, mocking concrete classes, and calling the system clock directly all trace back to the same root cause — from Khorikov, Chapter 11 "Unit testing anti-patterns".
Learn the functional-core/imperative-shell pattern for pulling business decisions out of side-effect-laden code so the decision logic becomes trivially output-based testable with zero mocks, and understand the real costs — a shell that still needs integration tests, allocation overhead from immutability in hot paths, and domain logic that can't always be cleanly separated this way.
The three ways a unit test can verify behavior — checking a returned output, checking resulting state, or checking a call made to a collaborator — and why output-based testing produces the highest-quality tests but only works for side-effect-free code, leaving state-based testing as the reasonable default and communication-based testing (mocks) as the rare exception.
A mock (outgoing command) and a stub (incoming query) answer different questions, and asserting interactions with a stub is a classic overspecification mistake. The deeper principle: mocking itself doesn't cause fragile tests — mocking an interaction that's an implementation detail (intra-system, no client goal behind it) rather than observable behavior (a call that crosses the application boundary and stays visible outside it) does. The fix isn't avoiding mocks, it's mocking only at true system boundaries.
Why an integration test should hit a real database (a managed dependency) but mock an SMTP server or message bus (an unmanaged dependency) — and why mocking a managed dependency quietly breaks resistance to refactoring.
Two genuinely different definitions of test "isolation" — mock every collaborator (London) vs. mock only shared/volatile dependencies (classical) — and why they disagree about what a unit even is.
A map of production code by complexity/domain significance and collaborator count — where unit testing pays off best, where it's a waste, and how the Humble Object pattern fixes the code that's genuinely hard to test.
Protection against regressions, resistance to refactoring, fast feedback, and maintainability — the four attributes that judge any automated test, and why no test can maximize all four at once.
How to distribute tests across levels — many fast unit tests at the base, fewer integration/system/acceptance tests toward the top — what to test at each level, and which tool fits each, from JUnit in Action, Third Edition, Ch. 22.
Writing behavior specifications in business-readable Gherkin (Given/When/Then) and binding them to Java step definitions with Cucumber — with today's io.cucumber + JUnit 5 Platform setup replacing the book's obsolete info.cukes/@RunWith(Cucumber.class), from JUnit in Action, Third Edition, Ch. 21.
The red-green-refactor cycle — write a failing test first, add the smallest code to pass, then refactor safely — driving design with JUnit 5, from JUnit in Action, Third Edition, Ch. 20.
Testing a JPA/Hibernate persistence layer — entity mapping, persistence.xml, and EntityManager queries against a test database — with the critical javax→jakarta package move (Hibernate 6 / Spring 6), the @DataJpaTest slice, and Testcontainers for real-database integration tests, from JUnit in Action, Third Edition, Ch. 19.4–19.6.
Testing data-access code against an in-memory database — from raw JDBC (Connection/PreparedStatement/ResultSet) to Spring's JdbcTemplate/NamedParameterJdbcTemplate — with notes on obsolete Class.forName driver loading, injected JdbcTemplate over JdbcDaoSupport, and the @JdbcTest slice, from JUnit in Action, Third Edition, Ch. 19.1–19.3.
Driving a Spring REST controller through MockMvc — simulated HTTP requests, status/content-type/jsonPath assertions, and a mocked data layer — with today's @WebMvcTest slice, @MockitoBean (replacing @MockBean), and the Spring 6 NestedServletException change, from JUnit in Action, Third Edition, Ch. 18.
Bootstrapping a full application context with @SpringBootTest — component scanning, auto-configuration, and bean autowiring — plus today's faster test slices (@WebMvcTest, @DataJpaTest) and the @MockBean→@MockitoBean change, from JUnit in Action, Third Edition, Ch. 17.
Loading a Spring ApplicationContext into a JUnit 5 test with @ExtendWith(SpringExtension.class) and @ContextConfiguration, injecting beans via @Autowired — with a note on today's @SpringJUnitConfig shortcut, from JUnit in Action, Third Edition, Ch. 16.
Driving a real browser through the WebDriver interface — element lookup, cross-browser parameterized tests, and the Page Object model — with notes on what changed from the book's Selenium 3 to today's Selenium 4, from JUnit in Action, Third Edition, Ch. 15.
The five extension points (conditional execution, lifecycle callbacks, parameter resolution, exception handling, instance postprocessing) behind @ExtendWith — the same mechanism MockitoExtension and SpringExtension use — from JUnit in Action, Third Edition, Ch. 14.
Stubs vs. mocks as two strategies for faking a collaborator, and how to create and verify mocks with Mockito — from JUnit in Action, Third Edition, Ch. 7–8.
Measuring coverage with JaCoCo, writing testable code (Law of Demeter, composition over conditionals), and how TDD/BDD/mutation testing fit the development cycle — from JUnit in Action, Third Edition, Ch. 6.
Unit vs. integration vs. system vs. acceptance testing, and black-box vs. white-box testing — the vocabulary behind how a test suite is organized, from JUnit in Action, Third Edition, Ch. 5.
How Platform, Jupiter, and Vintage fit together to run new and legacy tests side by side, plus a short JUnit 4-to-5 migration reference — from JUnit in Action, Third Edition, Ch. 3.1/3.3 (and a short Ch. 4 note).
The JUnit 5 test lifecycle, assertions vs. assumptions, nested/tagged tests, and parameterized/dynamic tests — from JUnit in Action, Third Edition, Ch. 2.