🌱 Spring Boot Q40 / 69

How do you write unit tests for Spring Boot applications?

AI-Powered Answer ✓ Answered

Writing effective unit tests is crucial for maintaining the quality and stability of Spring Boot applications. Spring Boot provides excellent support for testing, offering various annotations and utilities to streamline the process, from isolated unit tests to broader integration tests.

Key Dependencies and Annotations

For most Spring Boot projects, the spring-boot-starter-test dependency is automatically included. This starter brings in a suite of essential testing libraries, including JUnit 5 (Jupiter API and Engine), Mockito for mocking objects, AssertJ for fluent assertions, and Spring Test for Spring-specific testing utilities.

Common Annotations:

  • @SpringBootTest: Loads the full application context, suitable for integration tests.
  • @WebMvcTest: Focuses on the web layer, ideal for testing controllers without a full server.
  • @DataJpaTest: Configures an in-memory database and tests JPA repositories.
  • @ExtendWith(MockitoExtension.class) (JUnit 5) or @RunWith(MockitoJUnitRunner.class) (JUnit 4): Integrates Mockito with JUnit.
  • @Mock: Creates a mock instance for a given class or interface.
  • @InjectMocks: Injects mock objects into the fields of the annotated object.

Testing Plain Java Components (Services, Utils)

For plain Java classes that don't directly interact with the Spring context (e.g., utility classes, services with mocked dependencies), you don't need Spring-specific annotations. You can use standard JUnit and Mockito.

java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

// Assume we have a simple service and a dependency it uses
class MyService {
    private MyRepository repository;

    public MyService(MyRepository repository) {
        this.repository = repository;
    }

    public String getData(String id) {
        return "Processed: " + repository.findById(id);
    }
}

interface MyRepository {
    String findById(String id);
}

class MyServiceUnitTest {

    @Mock
    private MyRepository mockRepository;

    @InjectMocks
    private MyService myService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testGetData() {
        when(mockRepository.findById("123")).thenReturn("mockedData");

        String result = myService.getData("123");

        assertEquals("Processed: mockedData", result);
    }
}

Testing Spring Web Controllers with @WebMvcTest

@WebMvcTest is specifically designed for testing Spring MVC controllers. It auto-configures Spring MVC infrastructure and limits the beans to only the web layer components (e.g., controllers, @ControllerAdvice, WebMvcConfigurer implementations). You typically use it with MockMvc to simulate HTTP requests.

java
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.test.web.servlet.MockMvc;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// Assume a simple controller and a service dependency
@RestController
class MyController {
    private final MyService myService;

    public MyController(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/api/data/{id}")
    public String getData(@PathVariable String id) {
        return myService.getData(id);
    }
}

@WebMvcTest(MyController.class)
class MyControllerUnitTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean // This mocks MyService for the controller
    private MyService myService;

    @Test
    void testGetDataEndpoint() throws Exception {
        when(myService.getData("1")).thenReturn("Service Result for 1");

        mockMvc.perform(get("/api/data/1"))
                .andExpect(status().isOk())
                .andExpect(content().string("Service Result for 1"));
    }
}

Testing JPA Repositories with @DataJpaTest

@DataJpaTest is used to test JPA applications. By default, it will configure an in-memory embedded database, scan for @Entity classes, and configure Spring Data JPA repositories. It's useful for verifying repository queries and mappings without loading the entire application context.

java
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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

// Assume a simple Entity and Repository
@Entity
class MyEntity {
    @Id @GeneratedValue
    private Long id;
    private String name;

    // Getters, Setters, Constructors
}

interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
    Optional<MyEntity> findByName(String name);
}

@DataJpaTest
class MyEntityRepositoryUnitTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private MyEntityRepository repository;

    @Test
    void testSaveAndFindEntity() {
        MyEntity entity = new MyEntity();
        entity.setName("Test Name");
        entityManager.persist(entity);
        entityManager.flush();

        Optional<MyEntity> found = repository.findByName("Test Name");

        assertTrue(found.isPresent());
        assertEquals("Test Name", found.get().getName());
    }
}

Best Practices for Unit Testing

  • Isolate Units: A true unit test should test a single class in isolation. Use mocks for all its dependencies.
  • Fast Execution: Unit tests should run quickly. Avoid loading the full Spring context unless absolutely necessary for integration.
  • Deterministic Results: Tests should always produce the same result, regardless of the order or environment.
  • Descriptive Test Names: Name your tests clearly, indicating what scenario is being tested and what the expected outcome is.
  • Arrange-Act-Assert: Structure your tests into three phases: set up the test data and mocks (Arrange), execute the code under test (Act), and verify the results (Assert).