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 […]
Artikel oleh Wayan · Sumber: Kode Java ↗
Terjemahan artikel ini belum tersedia. Isi asli ditampilkan.
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("customer@example.com");
when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
Order result = orderService.createOrder("customer@example.com");
assertEquals("customer@example.com", 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("user@example.com"));
}
@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, "customer@example.com"));
mockMvc.perform(get("/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.customerEmail").value("customer@example.com"));
}
}
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, "alice@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
User result = userService.findUser(1L);
assertEquals("alice@example.com", 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("alice@example.com");
userRepository.save(user);
Optional<User> result = userRepository.findByEmail("alice@example.com");
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.
