How do I write unit tests for Spring components?

Writing Unit Tests for Spring Components

For Spring components, you usually want to test business logic without starting the full Spring application context. That means using JUnit 5 and Mockito for most unit tests.

Use Spring’s test support only when you need Spring-specific behavior such as dependency injection, MVC request handling, configuration binding, or persistence integration.


1. Unit Test a Spring @Service

Example service:

package com.example.order;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;

    public Order createOrder(String customerEmail) {
        if (customerEmail == null || customerEmail.isBlank()) {
            throw new IllegalArgumentException("Customer email is required");
        }

        Order order = new Order(customerEmail);
        return orderRepository.save(order);
    }
}

Unit test:

package com.example.order;

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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

    @Test
    void createOrderSavesOrder() {
        Order savedOrder = new Order("[email protected]");

        when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);

        Order result = orderService.createOrder("[email protected]");

        assertEquals("[email protected]", result.getCustomerEmail());
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    void createOrderRejectsBlankEmail() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> orderService.createOrder(" ")
        );

        assertEquals("Customer email is required", exception.getMessage());
    }
}

This is a true unit test because no Spring context is started.


2. Unit Test a Spring @Component

Example component:

package com.example.notification;

import org.springframework.stereotype.Component;

@Component
public class EmailValidator {

    public boolean isValid(String email) {
        return email != null && email.contains("@");
    }
}

Test:

package com.example.notification;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class EmailValidatorTest {

    private final EmailValidator emailValidator = new EmailValidator();

    @Test
    void returnsTrueForValidEmail() {
        assertTrue(emailValidator.isValid("[email protected]"));
    }

    @Test
    void returnsFalseForInvalidEmail() {
        assertFalse(emailValidator.isValid("invalid-email"));
        assertFalse(emailValidator.isValid(null));
    }
}

If a component has no dependencies, just instantiate it directly.


3. Unit Test a Spring MVC @Controller

For controllers, use @WebMvcTest. This loads only the MVC layer, not the whole application.

Example controller:

package com.example.order;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @GetMapping("/orders/{id}")
    public OrderResponse getOrder(@PathVariable Long id) {
        return orderService.getOrder(id);
    }
}

Controller test:

package com.example.order;

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.test.context.bean.override.mockito.MockitoBean;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private OrderService orderService;

    @Test
    void getOrderReturnsOrder() throws Exception {
        when(orderService.getOrder(1L))
                .thenReturn(new OrderResponse(1L, "[email protected]"));

        mockMvc.perform(get("/orders/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.customerEmail").value("[email protected]"));
    }
}

In newer Spring Boot versions, prefer @MockitoBean over the older @MockBean.


4. Unit Test Repository-Using Services

If your service depends on a Spring Data JPA repository, mock the repository in a unit test.

package com.example.user;

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.Optional;

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

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void findUserReturnsUser() {
        User user = new User(1L, "[email protected]");

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        User result = userService.findUser(1L);

        assertEquals("[email protected]", result.getEmail());
    }
}

Do not use a real database for a unit test. If you want to test repository mappings or queries, use an integration/slice test such as @DataJpaTest.


5. Test Spring Data JPA Repositories with @DataJpaTest

This is not a pure unit test, but it is the standard way to test repositories.

package com.example.user;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertTrue;

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void findByEmailReturnsUser() {
        User user = new User();
        user.setEmail("[email protected]");

        userRepository.save(user);

        Optional<User> result = userRepository.findByEmail("[email protected]");

        assertTrue(result.isPresent());
    }
}

Use this when you want to verify:

  • JPA mappings
  • repository query methods
  • custom JPQL/native queries
  • database constraints

6. Recommended Dependencies

For Maven, the common Spring Boot test starter is usually enough:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

It includes commonly used testing libraries such as:

  • JUnit Jupiter
  • AssertJ
  • Mockito
  • Spring Test
  • MockMvc support

7. Common Testing Patterns

Arrange, Act, Assert

@Test
void methodNameExpectedBehavior() {
    // Arrange
    when(repository.findById(1L)).thenReturn(Optional.of(entity));

    // Act
    Result result = service.doSomething(1L);

    // Assert
    assertEquals(expectedValue, result.value());
}

Verify interactions only when meaningful

verify(repository).save(any(Order.class));

Avoid verifying every single method call. Prefer verifying observable behavior.

Test exceptions

@Test
void throwsExceptionWhenUserNotFound() {
    assertThrows(
            UserNotFoundException.class,
            () -> userService.findUser(999L)
    );
}

8. Choosing the Right Test Type

Component Recommended test style
Plain utility/component Instantiate directly
@Service with dependencies JUnit 5 + Mockito
@Controller @WebMvcTest + MockMvc
Repository @DataJpaTest
Full application flow @SpringBootTest

Rule of Thumb

Use the smallest test scope that proves the behavior:

  • Business logic: plain JUnit + Mockito
  • Web layer: @WebMvcTest
  • Persistence layer: @DataJpaTest
  • End-to-end Spring wiring: @SpringBootTest

Most Spring component unit tests should not need @SpringBootTest.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.