Spring Boot Testing with JUnit 5 and Mockito: Complete Guide
Master Spring Boot testing with JUnit 5 and Mockito. Learn unit testing, integration testing, mocking dependencies, and test-driven development.
Moshiour Rahman
Advertisement
Why Testing Matters
Testing ensures your code works correctly, catches bugs early, and enables confident refactoring. Spring Boot provides excellent testing support out of the box.
Testing Pyramid
| Level | Speed | Scope | Tools |
|---|---|---|---|
| Unit | Fast | Single class | JUnit, Mockito |
| Integration | Medium | Multiple components | @SpringBootTest |
| E2E | Slow | Full application | TestRestTemplate |
Setup
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
This includes JUnit 5, Mockito, AssertJ, and Spring Test.
Unit Testing
Testing a Service
// Service to test
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public User createUser(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException("Email already exists");
}
User user = User.builder()
.name(request.getName())
.email(request.getEmail())
.build();
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser.getEmail());
return savedUser;
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
}
Unit Test with Mockito
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void createUser_Success() {
// Given
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
User savedUser = User.builder()
.id(1L)
.name("John")
.email("john@example.com")
.build();
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
User result = userService.createUser(request);
// Then
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("John");
assertThat(result.getEmail()).isEqualTo("john@example.com");
verify(userRepository).existsByEmail(request.getEmail());
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("john@example.com");
}
@Test
void createUser_DuplicateEmail_ThrowsException() {
// Given
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(DuplicateEmailException.class)
.hasMessage("Email already exists");
verify(userRepository, never()).save(any());
verify(emailService, never()).sendWelcomeEmail(anyString());
}
@Test
void getUserById_NotFound_ThrowsException() {
// Given
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> userService.getUserById(userId))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("999");
}
}
Mockito Features
Argument Matchers
// Any value
when(repository.findById(any())).thenReturn(Optional.of(user));
when(repository.findByName(anyString())).thenReturn(users);
when(repository.findByAge(anyInt())).thenReturn(users);
// Specific conditions
when(repository.findById(argThat(id -> id > 0))).thenReturn(Optional.of(user));
when(service.process(argThat(s -> s.startsWith("test")))).thenReturn(result);
// Combining matchers
when(repository.findByNameAndAge(eq("John"), anyInt())).thenReturn(users);
Verifying Interactions
// Verify method called
verify(repository).save(user);
// Verify call count
verify(repository, times(2)).findById(any());
verify(repository, never()).delete(any());
verify(repository, atLeastOnce()).findAll();
verify(repository, atMost(3)).save(any());
// Verify order
InOrder inOrder = inOrder(repository, emailService);
inOrder.verify(repository).save(any());
inOrder.verify(emailService).sendEmail(any());
// Verify no more interactions
verifyNoMoreInteractions(repository);
Argument Captors
@Captor
private ArgumentCaptor<User> userCaptor;
@Test
void createUser_CapturesCorrectUser() {
// Given
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
when(userRepository.existsByEmail(any())).thenReturn(false);
when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
// When
userService.createUser(request);
// Then
verify(userRepository).save(userCaptor.capture());
User captured = userCaptor.getValue();
assertThat(captured.getName()).isEqualTo("John");
assertThat(captured.getEmail()).isEqualTo("john@example.com");
}
Stubbing Consecutive Calls
when(repository.findById(1L))
.thenReturn(Optional.of(user1))
.thenReturn(Optional.of(user2))
.thenThrow(new RuntimeException("Error"));
// Or using thenAnswer for dynamic responses
when(repository.save(any())).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(++idCounter);
return user;
});
Testing Controllers
Using @WebMvcTest
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void getUser_Success() throws Exception {
User user = User.builder()
.id(1L)
.name("John")
.email("john@example.com")
.build();
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void getUser_NotFound() throws Exception {
when(userService.getUserById(999L))
.thenThrow(new UserNotFoundException("User not found"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
@Test
void createUser_Success() throws Exception {
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
User createdUser = User.builder()
.id(1L)
.name("John")
.email("john@example.com")
.build();
when(userService.createUser(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John"));
}
@Test
void createUser_ValidationError() throws Exception {
CreateUserRequest request = new CreateUserRequest("", "invalid-email");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
}
Integration Testing
Using @SpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
class UserIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void createAndGetUser() {
// Create user
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
ResponseEntity<User> createResponse = restTemplate.postForEntity(
"/api/users", request, User.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().getId()).isNotNull();
// Get user
Long userId = createResponse.getBody().getId();
ResponseEntity<User> getResponse = restTemplate.getForEntity(
"/api/users/" + userId, User.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("John");
}
}
Testing with Real Database
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_ReturnsUser() {
User user = User.builder()
.name("John")
.email("john@example.com")
.build();
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("john@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John");
}
}
Testing Data Layer
@DataJpaTest
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_Success() {
// Given
User user = User.builder()
.name("John")
.email("john@example.com")
.build();
entityManager.persistAndFlush(user);
// When
Optional<User> found = userRepository.findByEmail("john@example.com");
// Then
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John");
}
@Test
void findByEmail_NotFound() {
Optional<User> found = userRepository.findByEmail("notfound@example.com");
assertThat(found).isEmpty();
}
@Test
void findActiveUsers_ReturnsOnlyActiveUsers() {
User activeUser = User.builder().name("Active").email("active@test.com").active(true).build();
User inactiveUser = User.builder().name("Inactive").email("inactive@test.com").active(false).build();
entityManager.persist(activeUser);
entityManager.persist(inactiveUser);
entityManager.flush();
List<User> activeUsers = userRepository.findByActiveTrue();
assertThat(activeUsers).hasSize(1);
assertThat(activeUsers.get(0).getName()).isEqualTo("Active");
}
}
Testing Configuration
Using @TestConfiguration
@TestConfiguration
public class TestConfig {
@Bean
public EmailService emailService() {
return new FakeEmailService();
}
}
@SpringBootTest
@Import(TestConfig.class)
class UserServiceIntegrationTest {
// EmailService will use FakeEmailService
}
Test Properties
@SpringBootTest
@TestPropertySource(properties = {
"app.feature.enabled=true",
"app.cache.ttl=60"
})
class FeatureTest {
// Tests with custom properties
}
// Or using a properties file
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class ConfigTest {
}
Testing Async Code
@Service
public class AsyncService {
@Async
public CompletableFuture<String> processAsync(String input) {
return CompletableFuture.completedFuture("Processed: " + input);
}
}
@SpringBootTest
class AsyncServiceTest {
@Autowired
private AsyncService asyncService;
@Test
void processAsync_Success() throws Exception {
CompletableFuture<String> future = asyncService.processAsync("test");
String result = future.get(5, TimeUnit.SECONDS);
assertThat(result).isEqualTo("Processed: test");
}
}
Testing Security
@WebMvcTest(SecureController.class)
class SecureControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void accessSecureEndpoint_Unauthorized() throws Exception {
mockMvc.perform(get("/api/secure"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
void accessSecureEndpoint_Authorized() throws Exception {
mockMvc.perform(get("/api/secure"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void accessAdminEndpoint_AsAdmin() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
void accessAdminEndpoint_AsUser_Forbidden() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isForbidden());
}
}
Best Practices
Test Naming Convention
// Method: methodName_condition_expectedResult
@Test
void getUserById_ValidId_ReturnsUser() { }
@Test
void getUserById_InvalidId_ThrowsException() { }
@Test
void createUser_DuplicateEmail_ThrowsDuplicateException() { }
Arrange-Act-Assert Pattern
@Test
void calculateDiscount_PremiumUser_Returns20Percent() {
// Arrange
User user = User.builder().type(UserType.PREMIUM).build();
Order order = Order.builder().total(100.0).build();
// Act
double discount = discountService.calculate(user, order);
// Assert
assertThat(discount).isEqualTo(20.0);
}
Test Data Builders
public class UserTestBuilder {
private Long id = 1L;
private String name = "Default Name";
private String email = "default@example.com";
public static UserTestBuilder aUser() {
return new UserTestBuilder();
}
public UserTestBuilder withId(Long id) {
this.id = id;
return this;
}
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public User build() {
return User.builder()
.id(id)
.name(name)
.email(email)
.build();
}
}
// Usage
User user = aUser().withName("John").withEmail("john@test.com").build();
Summary
| Test Type | Annotation | Use Case |
|---|---|---|
| Unit | @ExtendWith(MockitoExtension.class) | Single class |
| Controller | @WebMvcTest | REST endpoints |
| Repository | @DataJpaTest | JPA queries |
| Integration | @SpringBootTest | Full context |
Effective testing makes your Spring Boot applications reliable and maintainable.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Spring Boot 3 Virtual Threads: Complete Guide to Java 21 Concurrency
Master virtual threads in Spring Boot 3. Learn configuration, performance benchmarks, when to use them, common pitfalls, and production-ready patterns for high-throughput applications.
Spring BootSpring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Spring BootSpring Security Architecture: Complete Guide to How Security Works
Master Spring Security internals. Learn FilterChain, SecurityContext, Authentication flow, and how Spring Security protects your application.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.