Skip to content

Unit and component

The runner matters least of anything on this page. Every stack has one that works, it is already chosen, and changing it buys nothing. What is worth deliberate choice is the assertion library, the double strategy, and whether property-based testing is used where it applies.

Stack Runner Assertions Doubles Property-based
Java JUnit 5 AssertJ Mockito jqwik
Kotlin JUnit 5 or Kotest Kotest matchers, AssertJ MockK Kotest property testing
TypeScript / JS Vitest built-in expect vi.mock, MSW at the edge fast-check
Python pytest plain assert unittest.mock, pytest-mock Hypothesis
Swift Swift Testing / XCTest #expect protocol doubles SwiftCheck
Kotlin / Android JUnit 5, Robolectric Kotest, Truth MockK Kotest

Vitest over Jest for anything Vite-based, which is most of the current front-end world: it shares the build pipeline, so the tests see the same transforms as the application, and it is fast enough to run in watch mode continuously.

AssertJ or Kotest matchers over bare JUnit assertions. Not a style preference — assertThat(total).isEqualTo(102.euros) produces a failure message containing both values, and assertTrue(total == 102) produces expected true. The assertion library is the failure message. See writing a test.

The level that gives the most and is most often missing — the module through its own boundary, in process, dependencies doubled.

Spring Boot@SpringBootTest with webEnvironment = MOCK and MockMvc, or sliced (@WebMvcTest, @DataJpaTest). Real wiring, real serialisation, real validation; no server, no network.

Quarkus@QuarkusTest with RestAssured. Fast, because the framework was designed for it.

React / front-end — Testing Library plus MSW. Testing Library’s queries are by role and accessible name, which makes accessibility regressions fail the functional test — see accessibility. MSW intercepts at the network layer rather than mocking the client, so the component exercises its real fetch path.

iOS / Android — the view model or presenter with its repositories doubled. Fast, no simulator, no device.

Underused, and the highest-value addition on this page for the code that deserves it.

Instead of one example, state a property that must hold for all inputs and let the tool generate hundreds — including the empty case, the maximum, the negative, the Unicode, and the combination nobody thought of. On failure it shrinks to the smallest reproducing case and prints the seed, so it stays reproducible despite being random. See determinism.

Where it earns its keep: parsers and serialisers (round-trip: parse(render(x)) == x), anything with invariants (a discount never makes a total negative; applying it twice equals applying it once), sorting and comparison, date arithmetic, and money.

Where it does not: business rules that are genuinely a list of cases. Property testing “gold members get 15%” produces one property and a lot of ceremony.

Two additions worth more than the framework

Section titled “Two additions worth more than the framework”

Architecture tests. ArchUnit for Java and Kotlin, dependency-cruiser or ESLint boundary rules for TypeScript, import-linter for Python. They assert structural rules — the domain does not import the framework; nothing outside this package touches that one; controllers do not talk to repositories. A dozen of them prevent the slow erosion that no code review catches consistently, because each individual violation looks reasonable.

Approval testing. For output too large or too structural to assert field by field — a rendered document, a complex response, a generated file. The first run records the output for a human to approve; subsequent runs diff against it. The danger is approving a wrong result, so the review of the first approval is the whole test.

PIT for the JVM, Stryker for TypeScript and .NET, mutmut or Cosmic Ray for Python. Nightly, on the critical modules only. See coverage for what it measures and why the percentage is not the output.