Spring Boot Testing: Complete Guide with JUnit & Mockito

Introduction
Testing is crucial for building reliable, maintainable Spring Boot applications. Comprehensive test coverage ensures your code works correctly, catches bugs early, and makes refactoring safer.
In this comprehensive guide, we'll cover:
- Testing fundamentals and test pyramid
- Unit testing with JUnit 5 and Mockito
- Integration testing with Spring Boot Test
- Testing REST APIs with MockMvc
- Database testing with TestContainers
- Security testing for authenticated endpoints
- Test-Driven Development (TDD) best practices
- Code coverage and continuous testing
Prerequisites: This tutorial builds on Spring Boot Security. Ensure you have a working Spring Boot application with JPA and Security configured.
The Testing Pyramid
A well-tested application follows the testing pyramid:
/\
/ \ E2E Tests (Few)
/____\
/ \
/ Integr \ Integration Tests (Some)
/__________\
/ \
/ Unit Tests \ Unit Tests (Many)
/________________\Unit Tests (70%): Fast, isolated tests for individual components
Integration Tests (20%): Test component interactions
End-to-End Tests (10%): Test complete user workflows
Setting Up Dependencies
Update your pom.xml:
<dependencies>
<!-- Spring Boot Test Starter (includes JUnit 5, Mockito, AssertJ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security Test -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- TestContainers for database testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<!-- H2 Database for in-memory testing (alternative to TestContainers) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- Existing dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>Unit Testing with JUnit 5
Testing Service Layer
Unit tests should be fast and isolated. Use Mockito to mock dependencies.
src/test/java/com/example/demo/service/UserServiceTest.java:
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService Unit Tests")
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User("johndoe", "john@example.com", "password123");
testUser.setId(1L);
testUser.setFirstName("John");
testUser.setLastName("Doe");
}
@Test
@DisplayName("Should create user successfully")
void testCreateUser_Success() {
// Given
when(userRepository.existsByUsername(anyString())).thenReturn(false);
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
User createdUser = userService.createUser(testUser);
// Then
assertThat(createdUser).isNotNull();
assertThat(createdUser.getUsername()).isEqualTo("johndoe");
assertThat(createdUser.getEmail()).isEqualTo("john@example.com");
verify(userRepository, times(1)).save(any(User.class));
}
@Test
@DisplayName("Should throw exception when username already exists")
void testCreateUser_UsernameExists() {
// Given
when(userRepository.existsByUsername("johndoe")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(testUser))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Username already exists");
verify(userRepository, never()).save(any(User.class));
}
@Test
@DisplayName("Should throw exception when email already exists")
void testCreateUser_EmailExists() {
// Given
when(userRepository.existsByUsername(anyString())).thenReturn(false);
when(userRepository.existsByEmail("john@example.com")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(testUser))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Email already exists");
verify(userRepository, never()).save(any(User.class));
}
@Test
@DisplayName("Should get user by ID successfully")
void testGetUserById_Success() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
// When
Optional<User> foundUser = userService.getUserById(1L);
// Then
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getUsername()).isEqualTo("johndoe");
verify(userRepository, times(1)).findById(1L);
}
@Test
@DisplayName("Should return empty when user not found")
void testGetUserById_NotFound() {
// Given
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// When
Optional<User> foundUser = userService.getUserById(999L);
// Then
assertThat(foundUser).isEmpty();
verify(userRepository, times(1)).findById(999L);
}
@Test
@DisplayName("Should get all active users")
void testGetAllActiveUsers() {
// Given
User user2 = new User("janedoe", "jane@example.com", "password456");
user2.setActive(true);
when(userRepository.findByActiveTrue()).thenReturn(Arrays.asList(testUser, user2));
// When
List<User> activeUsers = userService.getAllActiveUsers();
// Then
assertThat(activeUsers).hasSize(2);
assertThat(activeUsers).extracting(User::getUsername)
.containsExactly("johndoe", "janedoe");
verify(userRepository, times(1)).findByActiveTrue();
}
@Test
@DisplayName("Should update user successfully")
void testUpdateUser_Success() {
// Given
User updatedData = new User();
updatedData.setFirstName("Johnny");
updatedData.setLastName("Doe Jr.");
updatedData.setEmail("johnny@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
User updatedUser = userService.updateUser(1L, updatedData);
// Then
assertThat(updatedUser.getFirstName()).isEqualTo("Johnny");
assertThat(updatedUser.getLastName()).isEqualTo("Doe Jr.");
verify(userRepository, times(1)).save(testUser);
}
@Test
@DisplayName("Should throw exception when updating non-existent user")
void testUpdateUser_NotFound() {
// Given
User updatedData = new User();
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> userService.updateUser(999L, updatedData))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("User not found");
verify(userRepository, never()).save(any(User.class));
}
@Test
@DisplayName("Should deactivate user successfully")
void testDeactivateUser() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
userService.deactivateUser(1L);
// Then
assertThat(testUser.getActive()).isFalse();
verify(userRepository, times(1)).save(testUser);
}
@Test
@DisplayName("Should count active users")
void testCountActiveUsers() {
// Given
when(userRepository.countByActiveTrue()).thenReturn(5L);
// When
long count = userService.countActiveUsers();
// Then
assertThat(count).isEqualTo(5L);
verify(userRepository, times(1)).countByActiveTrue();
}
}Key Testing Concepts:
@ExtendWith(MockitoExtension.class): Enable Mockito@Mock: Create mock dependencies@InjectMocks: Inject mocks into the service@BeforeEach: Setup before each test@DisplayName: Readable test names- AssertJ: Fluent assertions (
assertThat()) - Mockito: Verify method calls
Integration Testing with Spring Boot Test
Integration tests verify components work together correctly.
Testing Repository Layer
src/test/java/com/example/demo/repository/UserRepositoryIntegrationTest.java:
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
@DataJpaTest
@DisplayName("UserRepository Integration Tests")
class UserRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User("johndoe", "john@example.com", "password123");
testUser.setFirstName("John");
testUser.setLastName("Doe");
}
@Test
@DisplayName("Should save and retrieve user")
void testSaveAndFindUser() {
// When
User savedUser = userRepository.save(testUser);
entityManager.flush();
// Then
assertThat(savedUser.getId()).isNotNull();
Optional<User> foundUser = userRepository.findById(savedUser.getId());
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getUsername()).isEqualTo("johndoe");
}
@Test
@DisplayName("Should find user by username")
void testFindByUsername() {
// Given
entityManager.persist(testUser);
entityManager.flush();
// When
Optional<User> foundUser = userRepository.findByUsername("johndoe");
// Then
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getEmail()).isEqualTo("john@example.com");
}
@Test
@DisplayName("Should find user by email")
void testFindByEmail() {
// Given
entityManager.persist(testUser);
entityManager.flush();
// When
Optional<User> foundUser = userRepository.findByEmail("john@example.com");
// Then
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getUsername()).isEqualTo("johndoe");
}
@Test
@DisplayName("Should find all active users")
void testFindByActiveTrue() {
// Given
User activeUser = new User("active", "active@example.com", "pass");
activeUser.setActive(true);
User inactiveUser = new User("inactive", "inactive@example.com", "pass");
inactiveUser.setActive(false);
entityManager.persist(activeUser);
entityManager.persist(inactiveUser);
entityManager.flush();
// When
List<User> activeUsers = userRepository.findByActiveTrue();
// Then
assertThat(activeUsers).hasSize(1);
assertThat(activeUsers.get(0).getUsername()).isEqualTo("active");
}
@Test
@DisplayName("Should check if username exists")
void testExistsByUsername() {
// Given
entityManager.persist(testUser);
entityManager.flush();
// When & Then
assertThat(userRepository.existsByUsername("johndoe")).isTrue();
assertThat(userRepository.existsByUsername("nonexistent")).isFalse();
}
@Test
@DisplayName("Should check if email exists")
void testExistsByEmail() {
// Given
entityManager.persist(testUser);
entityManager.flush();
// When & Then
assertThat(userRepository.existsByEmail("john@example.com")).isTrue();
assertThat(userRepository.existsByEmail("nonexistent@example.com")).isFalse();
}
@Test
@DisplayName("Should count active users")
void testCountByActiveTrue() {
// Given
User user1 = new User("user1", "user1@example.com", "pass");
User user2 = new User("user2", "user2@example.com", "pass");
User inactiveUser = new User("inactive", "inactive@example.com", "pass");
inactiveUser.setActive(false);
entityManager.persist(user1);
entityManager.persist(user2);
entityManager.persist(inactiveUser);
entityManager.flush();
// When
long count = userRepository.countByActiveTrue();
// Then
assertThat(count).isEqualTo(2);
}
@Test
@DisplayName("Should search users by first name")
void testFindByFirstNameContainingIgnoreCase() {
// Given
User john = new User("john1", "john1@example.com", "pass");
john.setFirstName("Johnny");
User jane = new User("jane1", "jane1@example.com", "pass");
jane.setFirstName("Jane");
entityManager.persist(john);
entityManager.persist(jane);
entityManager.flush();
// When
List<User> results = userRepository.findByFirstNameContainingIgnoreCase("joh");
// Then
assertThat(results).hasSize(1);
assertThat(results.get(0).getFirstName()).isEqualTo("Johnny");
}
}Key Concepts:
@DataJpaTest: Configures in-memory database for repository testsTestEntityManager: Allows direct database manipulation- Tests run in transactions (auto-rollback)
- Uses H2 or TestContainers for database
Testing REST Controllers with MockMvc
MockMvc allows testing controllers without starting a full server.
src/test/java/com/example/demo/controller/UserControllerTest.java:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.Optional;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
@DisplayName("UserController REST API Tests")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User("johndoe", "john@example.com", "password123");
testUser.setId(1L);
testUser.setFirstName("John");
testUser.setLastName("Doe");
}
@Test
@DisplayName("GET /api/users/{id} - Should return user when authenticated")
@WithMockUser
void testGetUser_Success() throws Exception {
// Given
when(userService.getUserById(1L)).thenReturn(Optional.of(testUser));
// When & Then
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.username", is("johndoe")))
.andExpect(jsonPath("$.email", is("john@example.com")))
.andExpect(jsonPath("$.firstName", is("John")))
.andExpect(jsonPath("$.lastName", is("Doe")));
verify(userService, times(1)).getUserById(1L);
}
@Test
@DisplayName("GET /api/users/{id} - Should return 404 when user not found")
@WithMockUser
void testGetUser_NotFound() throws Exception {
// Given
when(userService.getUserById(999L)).thenReturn(Optional.empty());
// When & Then
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
@Test
@DisplayName("GET /api/users/{id} - Should return 401 when not authenticated")
void testGetUser_Unauthorized() throws Exception {
// When & Then
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("GET /api/users - Should return all active users for admin")
@WithMockUser(roles = "ADMIN")
void testGetAllUsers_Success() throws Exception {
// Given
User user2 = new User("janedoe", "jane@example.com", "password456");
user2.setId(2L);
when(userService.getAllActiveUsers()).thenReturn(Arrays.asList(testUser, user2));
// When & Then
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].username", is("johndoe")))
.andExpect(jsonPath("$[1].username", is("janedoe")));
}
@Test
@DisplayName("GET /api/users - Should return 403 for non-admin users")
@WithMockUser(roles = "USER")
void testGetAllUsers_Forbidden() throws Exception {
// When & Then
mockMvc.perform(get("/api/users"))
.andExpect(status().isForbidden());
}
@Test
@DisplayName("PUT /api/users/{id} - Should update user successfully")
@WithMockUser
void testUpdateUser_Success() throws Exception {
// Given
User updatedUser = new User("johndoe", "john.updated@example.com", "password123");
updatedUser.setId(1L);
updatedUser.setFirstName("Johnny");
updatedUser.setLastName("Updated");
when(userService.updateUser(anyLong(), any(User.class))).thenReturn(updatedUser);
// When & Then
mockMvc.perform(put("/api/users/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedUser)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.firstName", is("Johnny")))
.andExpect(jsonPath("$.lastName", is("Updated")))
.andExpect(jsonPath("$.email", is("john.updated@example.com")));
verify(userService, times(1)).updateUser(anyLong(), any(User.class));
}
@Test
@DisplayName("DELETE /api/users/{id} - Should delete user for admin")
@WithMockUser(roles = "ADMIN")
void testDeleteUser_Success() throws Exception {
// Given
doNothing().when(userService).deleteUser(1L);
// When & Then
mockMvc.perform(delete("/api/users/1").with(csrf()))
.andExpect(status().isNoContent());
verify(userService, times(1)).deleteUser(1L);
}
@Test
@DisplayName("DELETE /api/users/{id} - Should return 403 for non-admin")
@WithMockUser(roles = "USER")
void testDeleteUser_Forbidden() throws Exception {
// When & Then
mockMvc.perform(delete("/api/users/1").with(csrf()))
.andExpect(status().isForbidden());
verify(userService, never()).deleteUser(anyLong());
}
@Test
@DisplayName("GET /api/users/count - Should return active user count")
@WithMockUser
void testCountActiveUsers() throws Exception {
// Given
when(userService.countActiveUsers()).thenReturn(10L);
// When & Then
mockMvc.perform(get("/api/users/count"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(content().string("10"));
}
@Test
@DisplayName("GET /api/users/search - Should search users by name")
@WithMockUser
void testSearchUsers() throws Exception {
// Given
when(userService.searchUsers("John")).thenReturn(Arrays.asList(testUser));
// When & Then
mockMvc.perform(get("/api/users/search")
.param("name", "John"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].firstName", is("John")));
}
}Key Concepts:
@WebMvcTest: Load only web layer (controllers)@MockBean: Mock service dependencies@WithMockUser: Simulate authenticated userMockMvc: Perform HTTP requestsjsonPath(): Assert JSON response structure.with(csrf()): Include CSRF token for mutating operations
Testing with TestContainers
TestContainers provides real database instances for integration tests.
TestContainers Configuration
src/test/java/com/example/demo/AbstractIntegrationTest.java:
package com.example.demo;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
public abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@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);
}
}Full Integration Test
src/test/java/com/example/demo/UserIntegrationTest.java:
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
@DisplayName("User Full Integration Tests")
@Transactional
class UserIntegrationTest extends AbstractIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
@DisplayName("Should create user end-to-end")
void testCreateUserEndToEnd() {
// Given
User newUser = new User("testuser", "test@example.com", "password123");
newUser.setFirstName("Test");
newUser.setLastName("User");
// When
User createdUser = userService.createUser(newUser);
// Then
assertThat(createdUser.getId()).isNotNull();
Optional<User> foundUser = userRepository.findById(createdUser.getId());
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
}
@Test
@DisplayName("Should find active users only")
void testFindActiveUsersOnly() {
// Given
User activeUser = new User("active", "active@example.com", "pass");
activeUser.setActive(true);
userRepository.save(activeUser);
User inactiveUser = new User("inactive", "inactive@example.com", "pass");
inactiveUser.setActive(false);
userRepository.save(inactiveUser);
// When
List<User> activeUsers = userService.getAllActiveUsers();
// Then
assertThat(activeUsers).hasSize(1);
assertThat(activeUsers.get(0).getUsername()).isEqualTo("active");
}
}Testing Authentication Endpoints
src/test/java/com/example/demo/controller/AuthControllerTest.java:
package com.example.demo.controller;
import com.example.demo.dto.LoginRequest;
import com.example.demo.dto.RegisterRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import static org.hamcrest.Matchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@DisplayName("Authentication API Tests")
class AuthControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("POST /api/auth/register - Should register new user")
void testRegister_Success() throws Exception {
// Given
RegisterRequest request = new RegisterRequest();
request.setUsername("newuser");
request.setEmail("new@example.com");
request.setPassword("password123");
request.setFirstName("New");
request.setLastName("User");
// When & Then
mockMvc.perform(post("/api/auth/register")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(content().string(containsString("registered successfully")));
}
@Test
@DisplayName("POST /api/auth/register - Should reject duplicate username")
void testRegister_DuplicateUsername() throws Exception {
// Given - First registration
RegisterRequest request1 = new RegisterRequest();
request1.setUsername("duplicate");
request1.setEmail("email1@example.com");
request1.setPassword("password123");
mockMvc.perform(post("/api/auth/register")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request1)))
.andExpect(status().isCreated());
// When & Then - Second registration with same username
RegisterRequest request2 = new RegisterRequest();
request2.setUsername("duplicate");
request2.setEmail("email2@example.com");
request2.setPassword("password456");
mockMvc.perform(post("/api/auth/register")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request2)))
.andExpect(status().isBadRequest())
.andExpect(content().string(containsString("Username")));
}
@Test
@DisplayName("POST /api/auth/login - Should login with valid credentials")
void testLogin_Success() throws Exception {
// Given - Register user first
RegisterRequest registerRequest = new RegisterRequest();
registerRequest.setUsername("logintest");
registerRequest.setEmail("login@example.com");
registerRequest.setPassword("password123");
mockMvc.perform(post("/api/auth/register")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated());
// When & Then - Login
LoginRequest loginRequest = new LoginRequest("logintest", "password123");
mockMvc.perform(post("/api/auth/login")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token", notNullValue()))
.andExpect(jsonPath("$.type", is("Bearer")))
.andExpect(jsonPath("$.username", is("logintest")));
}
@Test
@DisplayName("POST /api/auth/login - Should reject invalid credentials")
void testLogin_InvalidCredentials() throws Exception {
// Given
LoginRequest loginRequest = new LoginRequest("nonexistent", "wrongpassword");
// When & Then
mockMvc.perform(post("/api/auth/login")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isUnauthorized());
}
}Test-Driven Development (TDD)
TDD follows the Red-Green-Refactor cycle:
- Red: Write a failing test
- Green: Write minimal code to pass
- Refactor: Improve code while keeping tests green
TDD Example
// Step 1: Write failing test
@Test
void testCalculateDiscount() {
OrderService service = new OrderService();
BigDecimal price = new BigDecimal("100.00");
BigDecimal discount = service.calculateDiscount(price, 10);
assertThat(discount).isEqualByComparingTo(new BigDecimal("10.00"));
}
// Step 2: Implement minimal code
public BigDecimal calculateDiscount(BigDecimal price, int percentage) {
return price.multiply(new BigDecimal(percentage))
.divide(new BigDecimal(100));
}
// Step 3: Refactor (add validation, edge cases, etc.)Best Practices
1. Follow AAA Pattern
@Test
void testExample() {
// Arrange: Setup test data
User user = new User("test", "test@example.com", "pass");
// Act: Execute the method under test
User result = userService.createUser(user);
// Assert: Verify the outcome
assertThat(result).isNotNull();
}2. Use Descriptive Test Names
// Good
@Test
void shouldReturnUserWhenValidIdProvided() { }
// Bad
@Test
void test1() { }3. Test One Thing Per Test
// Good
@Test
void shouldValidateEmail() { }
@Test
void shouldValidatePassword() { }
// Bad
@Test
void shouldValidateUserInput() {
// Tests both email and password
}4. Use Test Fixtures
@BeforeEach
void setUp() {
testUser = new User("test", "test@example.com", "pass");
}5. Mock External Dependencies
@Mock
private EmailService emailService;
@Test
void shouldSendWelcomeEmail() {
userService.createUser(testUser);
verify(emailService).sendWelcomeEmail(testUser.getEmail());
}6. Use TestContainers for Real Dependencies
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");7. Organize Tests by Feature
src/test/java/
├── unit/
│ ├── service/
│ └── util/
├── integration/
│ ├── repository/
│ └── controller/
└── e2e/
└── scenarios/Code Coverage
Measure with JaCoCo
Add to pom.xml:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>Run tests with coverage:
mvn clean test
mvn jacoco:reportView report: target/site/jacoco/index.html
Coverage Goals
- Aim for 80%+ overall coverage
- Critical paths: 90%+ coverage
- Don't obsess over 100%: Focus on meaningful tests
Continuous Testing
Run Tests on Every Commit
# Pre-commit hook
#!/bin/bash
mvn test
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fiCI/CD Integration
GitHub Actions example:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
- name: Run tests
run: mvn test
- name: Generate coverage report
run: mvn jacoco:reportCommon Testing Pitfalls
1. Testing Implementation Details
// Bad: Tests internal implementation
@Test
void shouldCallRepositorySaveMethod() {
userService.createUser(user);
verify(userRepository).save(any());
}
// Good: Tests behavior
@Test
void shouldPersistUserSuccessfully() {
User created = userService.createUser(user);
assertThat(created.getId()).isNotNull();
}2. Slow Tests
// Bad: Thread.sleep in tests
@Test
void testAsync() throws InterruptedException {
asyncService.process();
Thread.sleep(5000); // Don't do this!
}
// Good: Use Awaitility
@Test
void testAsync() {
asyncService.process();
await().atMost(5, SECONDS)
.until(() -> asyncService.isComplete());
}3. Flaky Tests
// Bad: Depends on execution order
@Test
void test1() { database.insert(user); }
@Test
void test2() { assertThat(database.count()).isEqualTo(1); }
// Good: Each test is independent
@BeforeEach
void setUp() { database.clear(); }Conclusion
Comprehensive testing is essential for building reliable Spring Boot applications. Key takeaways:
- Follow the test pyramid: Many unit tests, some integration tests, few E2E tests
- Use JUnit 5 and Mockito: Standard testing frameworks for Java
- MockMvc for API testing: Test controllers without starting server
- TestContainers for integration: Real database instances
- Practice TDD: Write tests first, then implementation
- Aim for high coverage: But focus on meaningful tests
- Automate testing: CI/CD pipeline integration
Well-tested code is easier to maintain, refactor, and extend.
Resources
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.