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.
Per stack
Section titled “Per stack”| 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.
Component tests per stack
Section titled “Component tests per stack”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.
Property-based testing
Section titled “Property-based testing”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.
Mutation testing
Section titled “Mutation testing”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.