Understanding Spring Test Types in Microservice Projects
In a real-world Spring Boot microservice architecture, choosing the right kind of test can drastically affect test speed, confidence, and maintainability. This guide breaks down key test types used in Spring applications, their purposes, and best practices.
✅ 1. Unit Test (Pure Java)
Purpose: Test a single class/method in isolation without Spring context.
Characteristics:
No Spring involved
Fastest and most isolated
Good for testing: Utils, DTOs, Mappers
Example:
class JwtUtilTest {
private final JwtUtil jwtUtil = new JwtUtil("secret", 3600);
@Test
void testGenerateAndValidateToken() {
String token = jwtUtil.generateToken("user123");
assertTrue(jwtUtil.validateToken(token));
}
}
✅ 2. Unit Test with Spring Context
Purpose: Test logic that depends on Spring-managed beans or environment.
Use When:
You need
@Autowired
services or componentsSpring Security context or configuration is involved
Example: CustomUserDetailServiceIT
@SpringBootTest
@AutoConfigureWebClient
@Import(RestTemplateTestConfig.class)
class CustomUserDetailServiceIT {
@Autowired
private CustomUserDetailService customUserDetailService;
@Autowired
private RestTemplate restTemplate;
private MockRestServiceServer mockServer;
@BeforeEach
void setup() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
@Test
void loadUserByUsername_shouldReturnValidUserDetails_whenUserExists() {
String testEmail = "test@example.com";
MemberDTO mockMember = new MemberDTO();
mockMember.setEmail(testEmail);
mockMember.setPassword("securePassword");
mockMember.setRole("ROLE_USER");
ServiceResponse<MemberDTO> response = new ServiceResponse<>();
response.setSuccess(true);
response.setData(mockMember);
String jsonResponse;
try {
jsonResponse = new MappingJackson2HttpMessageConverter()
.getObjectMapper()
.writeValueAsString(response);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize mock response", e);
}
mockServer.expect(requestTo("http://MEMBER-SERVICE/member/findMemberByEmail"))
.andExpect(method(HttpMethod.POST))
.andExpect(content().string(testEmail))
.andRespond(withSuccess(jsonResponse, MediaType.APPLICATION_JSON));
UserDetails userDetails = customUserDetailService.loadUserByUsername(testEmail);
assertThat(userDetails.getUsername()).isEqualTo(testEmail);
assertThat(userDetails.getPassword()).isEqualTo("securePassword");
assertThat(userDetails.getAuthorities())
.extracting("authority")
.containsExactly("ROLE_USER");
mockServer.verify();
}
}
✅ 1. Unit Test (Pure Java, MapStruct Mapper)
Purpose: Test mapper logic without Spring context
Characteristics:
Uses
Mappers.getMapper(...)
Does not require
@SpringBootTest
Very fast and self-contained
Example: MemberMapperSpringTest.testUserDetailToMemberDTO()
class MemberMapperSpringTest {
private final MemberMapper memberMapper = Mappers.getMapper(MemberMapper.class);
@Test
void testUserDetailToMemberDTO() {
UserDetails userDetails = new UserDetails() {
@Override public String getUsername() { return "test@example.com"; }
@Override public String getPassword() { return "securePassword"; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
};
MemberDTO memberDTO = memberMapper.userDetailToMemberDTO(userDetails);
assertEquals(userDetails.getUsername(), memberDTO.getUsername());
assertEquals(userDetails.getPassword(), memberDTO.getPassword());
}
}
```java
class MemberMapperSpringTest {
private final MemberMapper memberMapper = Mappers.getMapper(MemberMapper.class);
@Test
void testUserDetailToMemberDTO() {
UserDetails userDetails = new UserDetails() {
@Override public String getUsername() { return "test@example.com"; }
@Override public String getPassword() { return "securePassword"; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
};
MemberDTO memberDTO = memberMapper.userDetailToMemberDTO(userDetails);
assertEquals(userDetails.getUsername(), memberDTO.getUsername());
assertEquals(userDetails.getPassword(), memberDTO.getPassword());
}
}
✅ 3. Mock Integration Test
Purpose: Simulate external service calls (e.g., REST) using mocks.
Use Tools Like:
MockRestServiceServer
(RestTemplate)@WebMvcTest
+MockMvc
Use When:
You want to test integration logic without hitting real services
You want fast feedback with controlled responses
Example: CustomUserDetailServiceIT
@SpringBootTest
@AutoConfigureWebClient
@Import(RestTemplateTestConfig.class)
class CustomUserDetailServiceIT {
@Autowired
private CustomUserDetailService customUserDetailService;
@Autowired
private RestTemplate restTemplate;
private MockRestServiceServer mockServer;
@BeforeEach
void setup() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
@Test
void loadUserByUsername_shouldReturnValidUserDetails_whenUserExists() {
// Setup mock response to MEMBER-SERVICE
mockServer.expect(requestTo("http://MEMBER-SERVICE/member/findMemberByEmail"))
.andRespond(withSuccess(...));
UserDetails userDetails = customUserDetailService.loadUserByUsername("test@example.com");
assertEquals("test@example.com", userDetails.getUsername());
}
}
✅ 4. Full Integration Test
Purpose: Run full Spring context and test real interactions between components (REST, DB, Eureka).
Use When:
You want to validate behavior across real modules
You rely on Eureka, DB, or Cloud Config
Example: AuthControllerIT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class AuthControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
void testRegister() throws Exception {
mockMvc.perform(post("/auth/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{"email":"test@example.com"}"))
.andExpect(status().isCreated());
}
}
✅ 5. End-to-End (E2E) Test
Purpose: Validate full user flow across services, databases, gateways, and possibly frontend.
Tools:
TestContainers
Docker Compose
Postman / Newman
Cypress (frontend)
Example:
User registration through API Gateway hitting multiple microservices
Summary Table
Test Type | Uses Spring? | Uses Real Beans? | External Systems? | Speed | Purpose |
---|---|---|---|---|---|
Unit Test (Pure) | ❌ No | ❌ No | ❌ No | ⚡ Fast | Logic, util, mapper |
Unit + Spring Context | ✅ Yes | ✅ Yes | ❌ No | ⏳ Medium | Spring-feature logic |
Mock Integration Test | ✅ Yes | ✅ Yes | ❌ Mocked | ⏳ Medium | External API clients (mocked) |
Full Integration Test | ✅ Yes | ✅ Yes | ✅ Yes | ⏱ Slow | Inter-module validation |
End-to-End Test | ✅ Full stack | ✅ All services | ✅ Yes | ⏰ Very Slow | User scenario simulation |
Conclusion
Choosing the right type of test is key to balancing speed, confidence, and coverage. Start with fast unit tests, mock where appropriate, and reserve integration tests for validating system behavior.
✨ Rule of Thumb: “Don’t use Spring if you don’t have to.”
Let me know if you’d like this turned into a Markdown blog post or visual diagram.