Java Testing
Overview
Tests document behavior. The house stack: JUnit Jupiter for structure, AssertJ for
assertions, Mockito (annotation-driven) for isolation, and Testcontainers +
@ServiceConnection when a test needs a real backing service. For container lifecycle and
suite-wide sharing details see references/testcontainers-patterns.md.
Structure & assertions
- JUnit Jupiter (5+) by default, unless the repo standardizes on something else.
- AssertJ for assertions —
assertThat(x).isEqualTo(y) / assertThatThrownBy(...), never JUnit's
assertEquals/assertTrue.
- Every test has a method-level
@DisplayName in natural language. Never at the class level
(including @Nested).
- The method name mirrors the
@DisplayName in camelCase and describes behavior, not
implementation: shouldDoXWhenY, shouldNotDoXWhenYIsCondition. Never a test prefix.
- One behavior per test; order-independent and parallel-safe (no shared mutable state).
- Prefer
@ParameterizedTest for input matrices and edge-case grids over duplicated bodies.
- Never skip/disable/comment out a test to make the build pass.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class NotificationServiceTest {
@DisplayName("Should save published event When the producer publishes a new event")
@Test
void shouldSavePublishedEventWhenTheProducerPublishesANewEvent() {
assertThat(service.publish(event).status()).isEqualTo(Status.SAVED);
}
}
Mockito
- Use the JUnit Jupiter integration:
@ExtendWith(MockitoExtension.class). Don't call
MockitoAnnotations.openMocks(...) or create mocks imperatively with mock(...).
- Prefer annotation mocks:
@Mock, @Spy, @Captor, @InjectMocks.
- Avoid brittle setups — no deep stubbing or mock-heavy tests tied to implementation details;
prefer fakes or real collaborators. Stub only inside the test that needs it (no global stubbing).
- For Spring-managed beans use
@MockitoBean (prefer over the older @MockBean).
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository repository;
@InjectMocks OrderService service;
}
@SpringBootTest
class PaymentControllerTest {
@MockitoBean PaymentService paymentService; // prefer over @MockBean
}
Integration tests (Testcontainers)
Integration tests exercise the real backing service against production parity, fast and isolated:
- Use a real engine via Testcontainers, not H2/in-memory. H2's compatibility mode hides
dialect/type/identity/
RETURNING differences — doubly true for Spring Data JDBC, which emits
SQL almost literally. Match the prod engine and major version (postgres:16-alpine).
- Wire it with
@ServiceConnection (Spring Boot 3.1+, needs the spring-boot-testcontainers
test dependency). It auto-creates the *ConnectionDetails bean — prefer it over manual
@DynamicPropertySource, which is the older, more verbose fallback.
- Container fields are
static. A non-static @Container starts a fresh container per test
method — slow. Static = once per class.
- Share containers across the whole suite, not per class — a shared singleton (or a Spring-managed
container in an imported
@TestConfiguration base) so the suite starts one Postgres, not one per
test class. See the reference.
- Isolate state between tests. Don't let one test's data leak into another. Use
@Transactional
rollback (repository slice tests) or reset (truncate / Flyway clean) in setup; never rely on order.
- Use the narrowest slice that exercises the behavior:
@DataJdbcTest (JDBC beans only, rolls
back per test) for repository tests; @SpringBootTest only when you need the full service path.
- Separate integration tests from unit tests so a slow container suite doesn't run on every
mvn test: name them *IT (Maven Failsafe) vs *Test (Surefire), and/or @Tag("integration").
- Don't hand-manage
start()/stop() for @Container fields (the extension owns the lifecycle) —
only for the deliberate singleton/reuse patterns in the reference.
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
// ... plus the usual JUnit/AssertJ/Spring imports
@Testcontainers
@SpringBootTest
class OrderPersistenceIT {
@Container
@ServiceConnection // auto-wires spring.datasource.* — no @DynamicPropertySource
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@DisplayName("Should persist order and read it back When place is called")
@Test
void shouldPersistOrderAndReadItBackWhenPlaceIsCalled() {
var placed = orderService.place(new Order(null, "customer-42", BigDecimal.valueOf(99.95)));
assertThat(orderRepository.findById(placed.id())).get()
.satisfies(found -> assertThat(found.customerId()).isEqualTo("customer-42"));
}
}
Common mistakes
| Rationalization |
Reality |
"assertEquals is fine / imported" |
House standard is AssertJ — reads as behavior, better failures. |
"@DisplayName on the @Nested class groups nicely" |
Class-level @DisplayName is forbidden; put it on each method. |
"testGet is clear enough" |
Behavior names: shouldReturnAccountWhenIdExists. No test prefix. |
"openMocks in @BeforeEach" |
Use @ExtendWith(MockitoExtension.class) + @Mock. |
| "Deep stubs keep the test short" |
Brittle — restructure or use a fake/real collaborator. |
"I'll @Disabled this flaky one" |
Never disable tests to go green; fix the test or the code. |
| "H2 in-memory is faster for tests" |
It hides real SQL/dialect bugs — Spring Data JDBC especially. Use the prod engine. |
"@DynamicPropertySource works fine" |
@ServiceConnection is the modern, terse replacement (Boot 3.1+). |
| "Each test/class gets its own container" |
Non-static @Container = container per method; per class is still slow at scale. static, shared across the suite. |
| "Integration tests run with the unit tests" |
Separate *IT/Failsafe from *Test/Surefire so mvn test stays fast. |
Red flags — stop
org.junit.jupiter.api.Assertions / assertEquals imported
@DisplayName on a class or @Nested class; a test-prefixed name; name ≠ its @DisplayName
MockitoAnnotations.openMocks, imperative mock(...), deep stubs, @MockBean when @MockitoBean exists
- Tests sharing static/mutable state, or depending on execution order / leftover data
H2/hsqldb/:mem: standing in for the production database
- A non-
static @Container; manual @DynamicPropertySource where @ServiceConnection applies
- A
@SpringBootTest where a @DataJdbcTest/@WebMvcTest slice would do
1---2name: java-testing3description: Use when writing or reviewing Java tests, unit or integration — JUnit Jupiter with AssertJ assertions, the @DisplayName / method-name mirroring convention, behavior-focused naming, Mockito (annotation mocks, no deep stubs, @MockitoBean for Spring), and Testcontainers with @ServiceConnection for real backing services (database, Kafka, Redis). Catches JUnit-assertion use, class-level @DisplayName, imperative or deep-stub mocking, H2-instead-of-real-DB, container-per-method, and slow/flaky suites.4---56# Java Testing78## Overview910Tests document behavior. The house stack: **JUnit Jupiter** for structure, **AssertJ** for11assertions, **Mockito** (annotation-driven) for isolation, and **Testcontainers** +12`@ServiceConnection` when a test needs a real backing service. For container lifecycle and13suite-wide sharing details see `references/testcontainers-patterns.md`.1415## Structure & assertions1617- **JUnit Jupiter** (5+) by default, unless the repo standardizes on something else.18- **AssertJ for assertions** — `assertThat(x).isEqualTo(y)` / `assertThatThrownBy(...)`, never JUnit's19 `assertEquals`/`assertTrue`.20- Every test has a **method-level `@DisplayName`** in natural language. **Never** at the class level21 (including `@Nested`).22- The **method name mirrors the `@DisplayName` in camelCase** and describes behavior, not23 implementation: `shouldDoXWhenY`, `shouldNotDoXWhenYIsCondition`. Never a `test` prefix.24- One behavior per test; **order-independent and parallel-safe** (no shared mutable state).25- Prefer **`@ParameterizedTest`** for input matrices and edge-case grids over duplicated bodies.26- Never skip/disable/comment out a test to make the build pass.2728```java29import org.junit.jupiter.api.DisplayName;30import org.junit.jupiter.api.Test;31import static org.assertj.core.api.Assertions.assertThat;3233class NotificationServiceTest {34 @DisplayName("Should save published event When the producer publishes a new event")35 @Test36 void shouldSavePublishedEventWhenTheProducerPublishesANewEvent() {37 assertThat(service.publish(event).status()).isEqualTo(Status.SAVED);38 }39}40```4142## Mockito4344- Use the JUnit Jupiter integration: **`@ExtendWith(MockitoExtension.class)`**. Don't call45 `MockitoAnnotations.openMocks(...)` or create mocks imperatively with `mock(...)`.46- Prefer **annotation mocks**: `@Mock`, `@Spy`, `@Captor`, `@InjectMocks`.47- **Avoid brittle setups** — no deep stubbing or mock-heavy tests tied to implementation details;48 prefer fakes or real collaborators. Stub only inside the test that needs it (no global stubbing).49- For Spring-managed beans use **`@MockitoBean`** (prefer over the older `@MockBean`).5051```java52@ExtendWith(MockitoExtension.class)53class OrderServiceTest {54 @Mock OrderRepository repository;55 @InjectMocks OrderService service;56}5758@SpringBootTest59class PaymentControllerTest {60 @MockitoBean PaymentService paymentService; // prefer over @MockBean61}62```6364## Integration tests (Testcontainers)6566Integration tests exercise the **real** backing service against production parity, fast and isolated:6768- **Use a real engine via Testcontainers, not H2/in-memory.** H2's compatibility mode hides69 dialect/type/identity/`RETURNING` differences — doubly true for **Spring Data JDBC**, which emits70 SQL almost literally. Match the prod engine and major version (`postgres:16-alpine`).71- **Wire it with `@ServiceConnection`** (Spring Boot 3.1+, needs the `spring-boot-testcontainers`72 test dependency). It auto-creates the `*ConnectionDetails` bean — **prefer it over manual73 `@DynamicPropertySource`**, which is the older, more verbose fallback.74- **Container fields are `static`.** A non-static `@Container` starts a fresh container *per test75 method* — slow. Static = once per class.76- **Share containers across the whole suite**, not per class — a shared singleton (or a Spring-managed77 container in an imported `@TestConfiguration` base) so the suite starts one Postgres, not one per78 test class. See the reference.79- **Isolate state between tests.** Don't let one test's data leak into another. Use `@Transactional`80 rollback (repository slice tests) or reset (truncate / Flyway clean) in setup; never rely on order.81- **Use the narrowest slice** that exercises the behavior: `@DataJdbcTest` (JDBC beans only, rolls82 back per test) for repository tests; `@SpringBootTest` only when you need the full service path.83- **Separate integration tests from unit tests** so a slow container suite doesn't run on every84 `mvn test`: name them `*IT` (Maven **Failsafe**) vs `*Test` (**Surefire**), and/or `@Tag("integration")`.85- Don't hand-manage `start()/stop()` for `@Container` fields (the extension owns the lifecycle) —86 only for the deliberate singleton/reuse patterns in the reference.8788```java89import org.springframework.boot.testcontainers.service.connection.ServiceConnection;90import org.testcontainers.containers.PostgreSQLContainer;91import org.testcontainers.junit.jupiter.Container;92import org.testcontainers.junit.jupiter.Testcontainers;93// ... plus the usual JUnit/AssertJ/Spring imports9495@Testcontainers96@SpringBootTest97class OrderPersistenceIT {9899 @Container100 @ServiceConnection // auto-wires spring.datasource.* — no @DynamicPropertySource101 static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");102103 @Autowired OrderService orderService;104 @Autowired OrderRepository orderRepository;105106 @DisplayName("Should persist order and read it back When place is called")107 @Test108 void shouldPersistOrderAndReadItBackWhenPlaceIsCalled() {109 var placed = orderService.place(new Order(null, "customer-42", BigDecimal.valueOf(99.95)));110 assertThat(orderRepository.findById(placed.id())).get()111 .satisfies(found -> assertThat(found.customerId()).isEqualTo("customer-42"));112 }113}114```115116## Common mistakes117118| Rationalization | Reality |119|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|120| "`assertEquals` is fine / imported" | House standard is AssertJ — reads as behavior, better failures. |121| "`@DisplayName` on the `@Nested` class groups nicely" | Class-level `@DisplayName` is forbidden; put it on each method. |122| "`testGet` is clear enough" | Behavior names: `shouldReturnAccountWhenIdExists`. No `test` prefix. |123| "`openMocks` in `@BeforeEach`" | Use `@ExtendWith(MockitoExtension.class)` + `@Mock`. |124| "Deep stubs keep the test short" | Brittle — restructure or use a fake/real collaborator. |125| "I'll `@Disabled` this flaky one" | Never disable tests to go green; fix the test or the code. |126| "H2 in-memory is faster for tests" | It hides real SQL/dialect bugs — Spring Data JDBC especially. Use the prod engine. |127| "`@DynamicPropertySource` works fine" | `@ServiceConnection` is the modern, terse replacement (Boot 3.1+). |128| "Each test/class gets its own container" | Non-static `@Container` = container per method; per class is still slow at scale. `static`, shared across the suite. |129| "Integration tests run with the unit tests" | Separate `*IT`/Failsafe from `*Test`/Surefire so `mvn test` stays fast. |130131## Red flags — stop132133- `org.junit.jupiter.api.Assertions` / `assertEquals` imported134- `@DisplayName` on a class or `@Nested` class; a `test`-prefixed name; name ≠ its `@DisplayName`135- `MockitoAnnotations.openMocks`, imperative `mock(...)`, deep stubs, `@MockBean` when `@MockitoBean` exists136- Tests sharing static/mutable state, or depending on execution order / leftover data137- `H2`/`hsqldb`/`:mem:` standing in for the production database138- A non-`static` `@Container`; manual `@DynamicPropertySource` where `@ServiceConnection` applies139- A `@SpringBootTest` where a `@DataJdbcTest`/`@WebMvcTest` slice would do