Java Testing Patterns
JUnit 5 Basics
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@Mock EmailService emailService;
@InjectMocks UserService userService;
@Test
void findById_returnsUser_whenExists() {
var user = new User(1L, "Alice", "alice@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
var result = userService.findById(1L);
assertThat(result).isEqualTo(user.toDto());
verify(userRepository).findById(1L);
}
@Test
void findById_throwsNotFound_whenMissing() {
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("99");
}
@Nested
class CreateUser {
@Test
void sendsWelcomeEmail_onSuccess() {
when(userRepository.existsByEmail(any())).thenReturn(false);
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
userService.create(new CreateUserRequest("Bob", "bob@example.com"));
verify(emailService).sendWelcome(argThat(u -> "bob@example.com".equals(u.getEmail())));
}
}
}
Parameterized Tests
@ParameterizedTest
@CsvSource({
"valid@email.com, true",
"invalid-email, false",
", false",
"missing@domain, false"
})
void validateEmail(String email, boolean expected) {
assertThat(EmailValidator.isValid(email)).isEqualTo(expected);
}
@ParameterizedTest
@MethodSource("provideOrders")
void calculateTotal(List<OrderItem> items, BigDecimal expected) {
assertThat(orderService.calculateTotal(items)).isEqualByComparingTo(expected);
}
static Stream<Arguments> provideOrders() {
return Stream.of(
Arguments.of(List.of(item(10, 2), item(5, 3)), new BigDecimal("35")),
Arguments.of(List.of(), BigDecimal.ZERO)
);
}
AssertJ Fluent Assertions
// Collections
assertThat(users)
.hasSize(3)
.extracting(User::getName)
.containsExactlyInAnyOrder("Alice", "Bob", "Charlie");
// Exceptions
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Input must not be null");
// Soft assertions — all failures reported at once
SoftAssertions.assertSoftly(soft -> {
soft.assertThat(user.getName()).isEqualTo("Alice");
soft.assertThat(user.getEmail()).endsWith("@example.com");
soft.assertThat(user.isActive()).isTrue();
});
Testcontainers
@SpringBootTest
@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired OrderRepository repository;
@Test
@Transactional
void savesAndRetrievesOrder() {
var order = new Order(/* ... */);
var saved = repository.save(order);
assertThat(repository.findById(saved.getId())).isPresent();
}
}
WireMock
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
class PaymentClientTest {
@Autowired PaymentClient paymentClient;
@Test
void chargeCard_returnsSuccess() {
stubFor(post(urlEqualTo("/v1/charges"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("1000")))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBodyFile("charge-success.json")));
var result = paymentClient.charge(new ChargeRequest("tok_test", 1000));
assertThat(result.getStatus()).isEqualTo("succeeded");
}
}
Spring MVC Test
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mvc;
@MockBean UserService userService;
@Test
void getUser_returns200() throws Exception {
when(userService.findById(1L)).thenReturn(new UserResponse(1L, "Alice"));
mvc.perform(get("/api/v1/users/1").accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
@Test
void createUser_returns201() throws Exception {
var body = """{"name":"Bob","email":"bob@example.com"}""";
when(userService.create(any())).thenReturn(new UserResponse(2L, "Bob"));
mvc.perform(post("/api/v1/users")
.contentType(APPLICATION_JSON)
.content(body))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(2));
}
}
Key Rules
@Nested classes group related tests — use them instead of long method name prefixes
@ParameterizedTest eliminates copy-paste test methods for boundary conditions
- Use
@MockBean in @WebMvcTest/@SpringBootTest; use @Mock + @ExtendWith(MockitoExtension.class) for pure unit tests
- Testcontainers
static container is reused across test methods in the class — far faster than per-test startup
- AssertJ
extracting() is cleaner than mapping to a list and then asserting — use it for collection element checks