Custom Spring Security Test in Spring Boot

When writing tests for Spring Security, it’s important to align your test scope with the actual application flow. Here’s how to test your CustomUserDetailService correctly in a Spring Boot microservices setup that communicates with another service via RestTemplate and is wired into the AuthenticationManager.


✅ Why Testing AuthenticationManager Is Better Than Calling loadUserByUsername()

Calling authenticationManager.authenticate(...):

  • ✅ Tests the full Spring Security flow.

  • ✅ Verifies that AuthenticationManager delegates to your CustomUserDetailService.

  • ✅ Validates Spring Security configurations like password encoders and roles.

  • ✅ Justifies the use of @SpringBootTest by requiring a real security context.

Calling customUserDetailService.loadUserByUsername() directly:

  • ❌ Only tests one method in isolation.

  • ❌ Does not verify integration with security chain.

  • ✅ Could be written as a unit test without Spring.


📅 Test Setup Summary

Service Under Test: CustomUserDetailService

  • Registered as a @Service

  • Wired into AuthenticationManager via a custom AuthenticationProvider

  • Loads user data via RestTemplate from MEMBER-SERVICE

Dependencies

  • RestTemplate is mocked with MockRestServiceServer

  • Test uses @SpringBootTest, @AutoConfigureWebClient, and a test profile


🖊️ Example Test Code: AuthenticationManager Test (Spring Security Context)

@SpringBootTest
@AutoConfigureWebClient
@Import(RestTemplateTestConfig.class)
class AuthenticationFlowIT {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RestTemplate restTemplate;

    private MockRestServiceServer mockServer;

    @BeforeEach
    void setup() {
        mockServer = MockRestServiceServer.createServer(restTemplate);
    }

    @Test
    void authenticationManager_shouldAuthenticate_whenValidUserExists() {
        String email = "test@example.com";
        String password = "securePassword";

        MemberDTO mockMember = new MemberDTO();
        mockMember.setEmail(email);
        mockMember.setPassword(password);
        mockMember.setRole("ROLE_USER");

        ServiceResponse<MemberDTO> response = new ServiceResponse<>();
        response.setSuccess(true);
        response.setData(mockMember);

        String json;
        try {
            json = new ObjectMapper().writeValueAsString(response);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        mockServer.expect(requestTo("http://MEMBER-SERVICE/member/findMemberByEmail"))
                  .andExpect(method(HttpMethod.POST))
                  .andRespond(withSuccess(json, MediaType.APPLICATION_JSON));

        Authentication result = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(email, password)
        );

        assertThat(result.isAuthenticated()).isTrue();
        assertThat(result.getName()).isEqualTo(email);
        mockServer.verify();
    }
}

🧪 Additional Comparison: loadUserByUsername() Direct Unit Test

class CustomUserDetailServiceUnitTest {

    private RestTemplate restTemplate = Mockito.mock(RestTemplate.class);
    private CustomUserDetailService customUserDetailService = new CustomUserDetailService(restTemplate);

    @Test
    void loadUserByUsername_shouldReturnValidUserDetails_whenMocked() {
        String email = "test@example.com";
        MemberDTO mockMember = new MemberDTO();
        mockMember.setEmail(email);
        mockMember.setPassword("securePassword");
        mockMember.setRole("ROLE_USER");

        Mockito.when(restTemplate.postForObject(
                Mockito.eq("http://MEMBER-SERVICE/member/findMemberByEmail"),
                Mockito.eq(email),
                Mockito.eq(MemberDTO.class)))
                .thenReturn(mockMember);

        UserDetails result = customUserDetailService.loadUserByUsername(email);

        assertThat(result.getUsername()).isEqualTo(email);
        assertThat(result.getPassword()).isEqualTo("securePassword");
        assertThat(result.getAuthorities()).extracting("authority").containsExactly("ROLE_USER");
    }
}

⚠️ Common Pitfalls

  • Calling loadUserByUsername() directly: Only valid for pure unit tests, not for validating authentication flow.

  • Using @SpringBootTest unnecessarily: Avoid full context startup unless you’re testing actual Spring features.

  • Skipping mock verification: Always call mockServer.verify() to ensure the mock request was made.


✨ Conclusion

To write meaningful Spring Security tests:

  • Use authenticationManager.authenticate(...) to simulate real login

  • Mock external service calls if needed (like member lookup)

  • Justify your use of @SpringBootTest with actual Spring behavior (e.g. security context)

  • Understand the flow from AuthenticationManagerAuthenticationProviderUserDetailsService

This approach gives confidence that your custom login logic works as intended during real user authentication.

Leave a Comment

Your email address will not be published. Required fields are marked *