Back to posts

Unit Testing Spring Boot Applications: A Complete Guide

Erik Nguyen / December 16, 2024

Unit Testing Spring Boot Applications: A Complete Guide

Testing is a crucial aspect of software development, and Spring Boot provides excellent support for testing at different levels. In this comprehensive guide, we'll explore how to effectively test various components of your Spring Boot application.

Test Dependencies

First, let's ensure we have the necessary dependencies in our pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Testing Controllers

Let's start with testing REST controllers. We'll use @WebMvcTest for lightweight MVC tests focusing on web layer components.

Sample Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        return ResponseEntity.status(HttpStatus.CREATED)
                           .body(userService.save(user));
    }
}

Controller Test

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void shouldReturnUserWhenExists() throws Exception {
        // Given
        User user = new User(1L, "John Doe", "john@example.com");
        when(userService.findById(1L)).thenReturn(user);

        // When & Then
        mockMvc.perform(get("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("John Doe"))
                .andExpect(jsonPath("$.email").value("john@example.com"))
                .andDo(print());
    }

    @Test
    void shouldCreateNewUser() throws Exception {
        // Given
        User userToCreate = new User(null, "Jane Doe", "jane@example.com");
        User createdUser = new User(1L, "Jane Doe", "jane@example.com");
        when(userService.save(any(User.class))).thenReturn(createdUser);

        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userToCreate)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("Jane Doe"));
    }
}

Mocking Services

Service layer testing involves unit testing your business logic while mocking dependencies.

Sample Service

@Service
public class UserService {

    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public User createUser(User user) {
        User savedUser = userRepository.save(user);
        emailService.sendWelcomeEmail(savedUser.getEmail());
        return savedUser;
    }
}

Service Test

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldCreateUserAndSendEmail() {
        // Given
        User user = new User(null, "John Doe", "john@example.com");
        User savedUser = new User(1L, "John Doe", "john@example.com");
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        doNothing().when(emailService).sendWelcomeEmail(anyString());

        // When
        User result = userService.createUser(user);

        // Then
        assertNotNull(result);
        assertEquals(savedUser.getId(), result.getId());
        verify(emailService).sendWelcomeEmail("john@example.com");
    }
}

Testing Repositories

For repository tests, we'll use @DataJpaTest which provides a convenient way to test JPA repositories.

Repository Test

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldSaveUser() {
        // Given
        User user = new User(null, "John Doe", "john@example.com");

        // When
        User savedUser = userRepository.save(user);

        // Then
        assertNotNull(savedUser.getId());
        assertEquals("John Doe", savedUser.getName());
    }

    @Test
    void shouldFindUserByEmail() {
        // Given
        User user = new User(null, "John Doe", "john@example.com");
        entityManager.persist(user);
        entityManager.flush();

        // When
        Optional<User> found = userRepository.findByEmail("john@example.com");

        // Then
        assertTrue(found.isPresent());
        assertEquals("John Doe", found.get().getName());
    }
}

Test Configuration

Application-Test Properties

Create application-test.properties or application-test.yml in your test resources:

# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

Custom Test Configuration

@TestConfiguration
public class TestConfig {

    @Bean
    public EmailService emailService() {
        return new MockEmailService();
    }

    @Bean
    @Primary
    public ObjectMapper testObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        return objectMapper;
    }
}

Integration Test Example

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UserIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setup() {
        userRepository.deleteAll();
    }

    @Test
    void shouldCreateAndRetrieveUser() throws Exception {
        // Create user
        User user = new User(null, "John Doe", "john@example.com");

        MvcResult createResult = mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(new ObjectMapper().writeValueAsString(user)))
                .andExpect(status().isCreated())
                .andReturn();

        // Get created user ID
        String response = createResult.getResponse().getContentAsString();
        Long userId = JsonPath.read(response, "$.id");

        // Verify user retrieval
        mockMvc.perform(get("/api/users/" + userId)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("John Doe"))
                .andExpect(jsonPath("$.email").value("john@example.com"));
    }
}

Best Practices

  1. Test Naming: Use descriptive test names that indicate the scenario being tested
@Test
void shouldReturnNotFoundWhenUserDoesNotExist()
@Test
void shouldThrowExceptionWhenEmailIsInvalid()
  1. Test Organization: Follow the AAA (Arrange-Act-Assert) pattern
@Test
void shouldCalculateOrderTotal() {
    // Arrange
    Order order = new Order();
    order.addItem(new Item("Book", 10.0));
    order.addItem(new Item("Pen", 5.0));

    // Act
    double total = order.calculateTotal();

    // Assert
    assertEquals(15.0, total, 0.01);
}
  1. Use Test Fixtures: Create helper methods for common test data setup
private User createTestUser() {
    return new User(null, "Test User", "test@example.com");
}

Common Testing Annotations

@SpringBootTest         // Full application context
@WebMvcTest            // MVC layer testing
@DataJpaTest           // Repository layer testing
@MockBean              // Add mock to Spring context
@SpyBean               // Add spy to Spring context
@AutoConfigureMockMvc  // Configure MockMvc
@ActiveProfiles        // Specify active profile
@Sql                   // Execute SQL scripts
@Transactional        // Roll back after each test

Conclusion

Effective testing is crucial for maintaining a reliable Spring Boot application. By following these patterns and practices, you can create a comprehensive test suite that helps ensure your application's quality and reliability.

Remember to:

  • Write tests at different levels (unit, integration)
  • Mock dependencies appropriately
  • Use appropriate test configurations
  • Follow testing best practices
  • Maintain test code quality

Additional Resources