We'll write two types of tests:
- Unit tests: they verify individual methods and components in isolation. No database or external services. Use mocks (Mockito) to simulate dependencies.
- Integration tests: they check how multiple components work together. For example, controllers talking to services and the database.
Getting ready to test
Testing dependencies
Spring Boot already includes what you need for basic testing. Still, make sure you have the following dependencies in your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
If you're using Gradle, the equivalent lines look like this:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.testcontainers:testcontainers'
After adding the dependencies, make sure the project reloads successfully.
Unit tests
Let's start by writing Unit tests for the service layer.
Code example: testing the service
Assume we have the following business logic in our service:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
}
Testing with Mockito
To test this class we need to isolate it from the database. We do that with mocks:
@ExtendWith(MockitoExtension.class) // Hook up Mockito integration
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenFound() {
// Arrange
Long userId = 1L;
User mockUser = new User(userId, "John Doe", "john.doe@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); // Mock the repository behavior
// Act
User result = userService.findUserById(userId);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
verify(userRepository, times(1)).findById(userId); // Verify that the method was called exactly once
}
@Test
void shouldThrowExceptionWhenUserNotFound() {
// Arrange
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(UserNotFoundException.class, () -> userService.findUserById(userId));
}
}
What's happening here?
- We use the
@Mockannotation to create a "fake" UserRepository. - With
@InjectMockswe inject that mock into UserService. - In the tests we set up the repository behavior (
when(...).thenReturn(...)) to create a controlled environment. - We check that the method works correctly and throws an exception when the user isn't found.
Integration tests
Now let's move on to more complex cases where we need to verify component interaction. We'll use a real database (for example, H2) and test controllers and services together.
Setting up integration tests
For integration testing controllers, Spring Boot gives you a handy tool: @SpringBootTest. It boots up the application context so you can test layer interactions.
Example REST controller under test:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findUserById(id);
return ResponseEntity.ok(user);
}
}
Integration test for the controller
Let's write a test that hits our API directly:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Start with a web server
@AutoConfigureMockMvc // Configure MockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnUserWhenExists() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
Debugging tests and common mistakes
- Context configuration error: make sure your
@SpringBootTestconfigs and mocks are set up correctly. - Lazy loading of data: sometimes integration tests fail because entity relationships are lazily loaded. Fix this by forcing explicit loading (FetchType.EAGER) or using DTOs.
- Slow tests: if your tests are slow, consider using Testcontainers to isolate tests in containers locally.
Automating tests
Hook tests into your CI/CD pipeline. On every commit your Unit and integration tests should run to prevent bugs from getting into the main branch. Most tools (Jenkins, GitLab CI, GitHub Actions) integrate easily with Maven or Gradle.
# Example GitLab CI pipeline
stages:
- test
test:
script:
- ./mvnw test
This is how you can gain confidence in your app before deploying it. Now you know how to write both Unit and integration tests. So, ready for the next step? Time to dive into containerization and build automation! 🚀
GO TO FULL VERSION