Skip to content
End To End Tester
Languagespractical

Writing Tests in Java

JUnit 5, Mockito, AssertJ and Testcontainers — the stack that has been stable for a decade, plus Spring's test slices and what they cost.

1 min read · updated 19 September 2026

Java's testing stack has been stable for a decade, which is unusual and useful: the answers you find are generally still correct.

#The stack

kotlin
// build.gradle.kts
dependencies {
    testImplementation(platform("org.junit:junit-bom:5.11.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testImplementation("org.assertj:assertj-core:3.26.3")
    testImplementation("org.mockito:mockito-junit-jupiter:5.14.0")
    testImplementation(platform("org.testcontainers:testcontainers-bom:1.20.3"))
    testImplementation("org.testcontainers:junit-jupiter")
    testImplementation("org.testcontainers:postgresql")
    testImplementation("org.wiremock:wiremock-standalone:3.9.1")
}

tasks.test {
    useJUnitPlatform()
    systemProperty("junit.jupiter.execution.parallel.enabled", "true")
}

#Unit tests

java
class PricingTest {

    @ParameterizedTest(name = "an order of {0} is discounted by {1}")
    @CsvSource({ "12000, 2400", "10000, 2000", "9999, 0" })
    void discountAppliesAtOrAboveTheThreshold(int subtotal, int expected) {
        var result = Pricing.applyDiscount(new Order(subtotal), POLICY);

        assertThat(result.discountCents()).isEqualTo(expected);
    }

    @Test
    void reservingMoreThanStockNamesTheSku() {
        var service = new OrderService(new StubInventory(1), FIXED_CLOCK);

        assertThatThrownBy(() -> service.reserve("book-1", 2))
            .isInstanceOf(InsufficientStockException.class)
            .hasMessageContaining("book-1");
    }
}

AssertJ carries most of the readability. Its collection assertions in particular are worth learning properly:

java
assertThat(order.lines())
    .hasSize(2)
    .extracting(OrderLine::sku, OrderLine::quantity)
    .containsExactlyInAnyOrder(tuple("book-1", 2), tuple("pen-2", 1));

#java.time and the Clock

java
// Take a Clock. It is in the standard library precisely for this.
public record TokenService(Clock clock) {
    public boolean isExpired(Token token) {
        return !token.expiresAt().isAfter(clock.instant());
    }
}

@Test
void aTokenExpiresExactlyAtItsExpiry() {
    var at = Instant.parse("2026-01-01T13:00:00Z");
    var service = new TokenService(Clock.fixed(at, ZoneOffset.UTC));

    assertThat(service.isExpired(new Token(at))).isTrue();
    assertThat(service.isExpired(new Token(at.plusSeconds(1)))).isFalse();
}

#Mockito

java
@ExtendWith(MockitoExtension.class)
class CheckoutTest {

    @Mock PaymentGateway gateway;
    @Captor ArgumentCaptor<ChargeRequest> request;

    @Test
    void chargesTheOrderTotalWithAnIdempotencyKey() {
        when(gateway.charge(any())).thenReturn(ChargeResult.succeeded("pi_1"));

        new Checkout(gateway).pay(order);

        verify(gateway).charge(request.capture());
        assertThat(request.getValue().cents()).isEqualTo(4_000);
        assertThat(request.getValue().idempotencyKey()).startsWith("ORD-");
    }
}

ArgumentCaptor plus AssertJ beats a complicated argument matcher every time: the failure message names the field that was wrong rather than saying "wanted but not invoked".

#Testcontainers

java
@Testcontainers
class OrderRepositoryTest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");

    static DataSource dataSource;

    @BeforeAll
    static void migrate() {
        dataSource = dataSourceFor(POSTGRES);
        Flyway.configure().dataSource(dataSource).load().migrate();
    }

    @Test
    void rejectsADuplicateReference() {
        var repository = new OrderRepository(dataSource);
        repository.save(new Order("REF-1", 4_000));

        assertThatThrownBy(() -> repository.save(new Order("REF-1", 5_000)))
            .isInstanceOf(DuplicateReferenceException.class);
    }
}

Testcontainers was born in this ecosystem and is best supported here. See Testcontainers.

#Spring: slices versus the full context

java
// A slice: the web layer only. Starts in ~1s.
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mvc;
    @MockitoBean OrderService service;      // @MockBean in older Spring Boot

    @Test
    void returnsA404ForAnUnknownOrder() throws Exception {
        when(service.find("nope")).thenReturn(Optional.empty());

        mvc.perform(get("/orders/nope"))
           .andExpect(status().isNotFound())
           .andExpect(jsonPath("$.error").value("not_found"));
    }
}
java
// The full application, on a real port, with a real database.
// This is a component test — see /testing-levels/component-testing.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class OrdersApiTest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void datasource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES::getUsername);
        registry.add("spring.datasource.password", POSTGRES::getPassword);
    }

    @Autowired TestRestTemplate rest;
    @MockitoBean PaymentGateway gateway;    // the only thing faked

    @Test
    void aDeclinedCardReturns402AndLeavesNoOrder() {
        when(gateway.charge(any())).thenReturn(ChargeResult.declined());

        var response = rest.postForEntity("/orders", new OrderRequest("book-1", 1), Problem.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.PAYMENT_REQUIRED);
        assertThat(rest.getForObject("/orders", OrderView[].class)).isEmpty();
    }
}

#The context caching trap

Spring caches the application context between test classes — but keyed on the configuration. Any difference creates a new context and another startup.

java
// These three produce three separate contexts, and three startups.
@SpringBootTest                                     class A { }
@SpringBootTest @MockitoBean(PaymentGateway.class)  class B { }
@SpringBootTest @TestPropertySource(properties = "feature.x=true") class C { }

A suite with thirty distinct configurations starts Spring thirty times, and that is almost always why a Spring test suite is slow. The fix is to standardise: a small number of shared test configurations, applied via a composed annotation.

java
@Target(TYPE) @Retention(RUNTIME)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Testcontainers
public @interface ComponentTest { }      // one context, shared by every user

#Running it

bash
./gradlew test                                # unit
./gradlew test -PexcludeTags=integration      # fast subset
./gradlew jacocoTestReport                    # coverage
mvn test -Dgroups='!integration'

Tag-based splitting — @Tag("integration") — is how you keep the inner loop fast while running everything in CI. See JUnit 5 for the tagging and parallelism configuration.

Common questions

What is the standard Java testing stack?
JUnit 5 for the runner, Mockito for doubles, AssertJ for assertions, Testcontainers for real dependencies, and WireMock for HTTP stubbing. It has been stable for years and there is little reason to deviate.
Should I use @SpringBootTest or a test slice?
A slice where one exists — @WebMvcTest, @DataJpaTest, @JsonTest — because they start a fraction of the context and run in a fraction of the time. Use the full @SpringBootTest for component tests where the whole wiring is the point.
Why are my Spring tests slow?
Usually context caching being defeated. Spring caches the application context between test classes, but any difference in configuration — a different @MockBean, a different property — creates a new one. A suite with thirty distinct context configurations starts Spring thirty times.

Runnable samples for this page

last test results ↗
  • Javajava/src/test/java/languages/java

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?