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
@MockitoBeanover 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.
