Creating a minimal Spring Security setup for JUnit unit tests can be a tedious as well as somewhat daunting task. With its Authentication
, Principal
, GrantedAuthority
, and UserDetails
classes Spring Security is extremely flexible and configurable and probably accommodates pretty much every authentication and authorization use case under the sun.
However, in software architecture such flexibility often comes at price, the trade-off in this case being that simple use cases can require more implementation effort than one would expect. One such use case is mocking an authenticated user for unit tests.
While Spring Security already provides a convenient @WithMockUser annotation that does just that, that annotation only becomes active if a valid SecurityContext
is available in our test environment.
Now, we might be tempted to use the @WebMvcTest Spring Boot slice, which indeed will auto-configure our security rules and allow us to easily mock the authenticated user.
However, that’s not how @WebMvcTest
was intended to be used. The purpose of this annotation is to provide a valid environment for Spring Web MVC integration tests, e.g., tests that deal with the @Controller
, @ControllerAdvice
, or WebMvcConfigurer
parts of an application.
If we just want to write a unit test that has nothing to do with HTTP endpoints or REST APIs, yet still requires authentication, using @WebMvcTest
is a less than optimal solution. Not only will tests annotated as such run longer due to the additional Spring Boot context setup involved, but due to Spring’s dependency injection we also might run into unexpected behaviour when initialising or mocking the dependencies of our system under test. Usually, with JUnit tests, nowadays we’d use Mockito‘s @Mock or @MockBean annotations for mocking dependencies. However, we should avoid using @Mock
and @MockBean
in tandem in a single test class or using @MockBean
for unit tests (as opposed to integration tests that require an actual Spring Boot environment).
Hence, a better approach is to do without @WebMvcTest
in such cases and provide a minimal SecurityContext
setup ourselves. Long story short, this is how to achieve just that:
1 2 3 4 5 6 7 8 9 10 11 | @Test void testMethod() throws Exception { SecurityContextHolder.setContext( SecurityContextHolder.createEmptyContext() ); SecurityContextHolder.getContext().setAuthentication( new TestingAuthenticationToken("TEST_USER", null) ); } |
Using the SecurityContextHolder
provided by Spring Security we create an empty SecurityContext
and afterwards use a TestingAuthenticationToken
for setting the username as previously defined in the @WithMockUser
annotation for the test method in question. This allows us to easily test for the username of an authenticated user without having to worry about the actual means of authentication or the – in many cases probably rather complex – context setup involved.
We might further refactor that setup to simplify our test method by providing a reusable factory responsible for that setup and simply calling a factory method where applicable:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class SecurityContextFactory { public static void createSecurityContext(String username) { SecurityContextHolder.setContext( SecurityContextHolder.createEmptyContext() ); SecurityContextHolder.getContext().setAuthentication( new TestingAuthenticationToken(username, null) ); } } |
1 2 3 4 5 6 7 8 9 10 11 | @ExtendWith(MockitoExtension.class) class SomeTest { private static final String TEST_USER = "TEST_USER"; @Test void recipientListEntryReceivedEvent() throws Exception { SecurityContextFactory.createSecurityContext(TEST_USER); } } |
Here are two GitHub Gists for the code shown above:
Thanks a lot to Philip Riecks, who pointed me in the right direction. Philip offers a highly recommended Testing Spring Boot Applications Masterclass you can check out here.