CodeGym /Kursy /Spring Security /MockMvc and a real SecurityFilterChain

MockMvc and a real SecurityFilterChain

Spring Security
Poziom 26, Lekcja 1
Dostępny

1. Connecting MockMvc to a real SecurityFilterChain

When we start writing tests, the brain, especially the brain of a beginner developer, wants one thing: for the tests to be green, for the build to pass, and for life to feel manageable. The problem is that security tests can become “green” for the wrong reason: you accidentally test the controller outside security, and the tests confirm not security, but your ability to slip past the turnstile through the service entrance.

Imagine a real-world scenario. In production, a request goes like this: first the filters, then Spring Security decides whether to “allow / not allow”, and only then does it get to the DispatcherServlet and the controller. If in a test you check the controller directly or build MockMvc in “controller only” mode, it is as if you are testing a museum with the metal detector removed. The museum becomes very “friendly”... but that is a questionable compliment.

As a result, especially tricky bugs appear: you wrote a test “public endpoint returns 200”, and it is green even if security is not connected at all, because public still returns 200. Then you add a rule to SecurityFilterChain, accidentally close the public area, and the test suddenly starts failing. You do not understand “what broke”: the endpoint? security? the tests? In reality, the basic assumption broke: your test environment should be as close as possible to the real web environment, otherwise tests turn into fiction.

2. MockMvc in the request lifecycle and filters

People often think of MockMvc as “well, it is basically an HTTP request, just without a server.” And that is almost true. But the key word here is almost. MockMvc really does let you make GET/POST/PATCH requests and get a response without starting Tomcat/Jetty/Netty. However, it tries to faithfully reproduce the server-side request-handling path inside Spring MVC. And on the server-side path, filters always come before the controller.

Let us fix a simple diagram. In a real application, a request comes from the client, enters the servlet filter chain, then, if it is not “rejected”, reaches the DispatcherServlet, which then routes everything to your controller. Spring Security lives exactly in the filters: it is not “an annotation on the controller”, but a full filter infrastructure.

If MockMvc is assembled correctly, the test request will go through a path roughly like this:

flowchart TD
    T["JUnit Jupiter test"] --> M["MockMvc.perform(...)"]
    M --> F["Servlet filters
including springSecurityFilterChain"] F --> D["DispatcherServlet"] D --> C["Controller"] C --> R["MockMvcResult / response"]

If MockMvc is assembled incorrectly, a common case is standaloneSetup, and then the Servlet filters block disappears. And you are no longer testing the “application”, but a “piece of the application”.

So here is a practical rule for today: security tests must run the request through the real SecurityFilterChain, otherwise you are not testing security, you are testing hope.

3. Environment setup and MockMvc assembly

3.1. Project preparation: dependencies for security tests

Before we start assembling MockMvc, we need to make sure we actually have the tools for security-aware tests. In the Spring world, this looks like a separate thought: Spring Test gives us MockMvc, and Spring Security separately provides a module that can be integrated correctly into the test infrastructure. And yes, if you forget the dependency, everything will be like in real life: “I seem to be doing everything right, but why is springSecurity() not found?”

In build.gradle.kts or build.gradle, you usually need the standard Spring Boot test stack and a separate spring-security-test dependency. Within Boot dependency management, you do not need to specify versions — Boot will choose compatible ones automatically.

Example:

dependencies {
    // Basic test stack for Spring Boot (JUnit, AssertJ, MockMvc, etc.)
    testImplementation("org.springframework.boot:spring-boot-starter-test")

    // Spring Security integration with the test infrastructure (springSecurity(), @WithMockUser, user(), etc.)
    testImplementation("org.springframework.security:spring-security-test")
}

If you see in the IDE that SecurityMockMvcConfigurers.springSecurity() does not resolve, or you do not have user() / @WithMockUser, then in 9 cases out of 10 the problem is exactly here. And yes, this is the moment when “one line in Gradle” saves your evening and your nervous system.

3.2. The basic path for the course: @SpringBootTest + @AutoConfigureMockMvc

The calmest start for a Spring Boot project is not to reinvent the wheel, but to take a basic test template that loads the application context and gives you a ready MockMvc. This is especially important in a security course: we need a real SecurityFilterChain, and in a normal application it is part of the context, with beans, settings, error handlers, and everything else you have already assembled in the project.

In our course, we usually start with the following test skeleton:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest // Load the full Spring Boot context (including the real SecurityFilterChain)
@AutoConfigureMockMvc // Register MockMvc as a bean and wire in the filters
class SecurityAccessTests {

    @Autowired
    MockMvc mockMvc; // We use this MockMvc to make requests as if they were going through the real web environment
}

A few important and practical observations. @SpringBootTest loads the Spring Boot context, which means you get the same SecurityFilterChain as in runtime. @AutoConfigureMockMvc adds a MockMvc bean to the context, already configured to work with Spring MVC.

If you want tests to be more predictable in terms of configuration, you often add @ActiveProfiles("test") to use a separate application-test.yml. But today we are keeping the focus on the main thing: the request in the test must pass through the same security filters as in the application.

3.3. Manual MockMvc assembly and springSecurity()

Auto-configuration is convenient, but sometimes life brings situations where MockMvc needs to be assembled manually: for example, you want a bit more control, or you are not using Spring Boot tests. And here the classic trap appears: you assembled MockMvc, it executes requests, tests run... but security does not participate in those requests.

For manual assembly to be done “properly”, you need to build MockMvc not through standaloneSetup, but through WebApplicationContext. And you must apply springSecurity() — this is a special test configurer that correctly connects Spring Security and MockMvc.

Minimal example:

import org.junit.jupiter.api.BeforeEach; 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

class SecurityAccessTests {

    @Autowired
    WebApplicationContext context; // Real web application context (with registered filters)

    MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = webAppContextSetup(context)
                // Important: connect Spring Security Test integration,
                // otherwise requests may go "around" the SecurityFilterChain
                .apply(springSecurity())
                .build();
    }
}

What matters here is not magic, but engineering honesty. webAppContextSetup(context) means: “build me a test web environment based on the real Spring context, with registered filters.” .apply(springSecurity()) means: “plug Spring Security Test integration into this environment so that the security context and filters work properly.”

But standaloneSetup(...) in security tests is almost always a bad idea, because it is convenient for testing a controller as a “plain class”, but by definition it does not have to know anything about your filter infrastructure. In other words, it tests the “controller”, not the “application at the boundary”.

4. Smoke tests: checking that security is connected

For now, this is not a regression pack. The task here is simpler: quickly verify that the test environment actually routes the request through security and honestly shows the boundary between public and protected endpoints.

Before writing a nice test matrix for USER/EDITOR/ADMIN, it is useful to make two short checks that play the role of a “power indicator”. These are not “full security tests”; they are diagnostics: “Did I actually connect the filters, or am I running through an empty hallway?”

The first test is usually taken from the public zone. If public reading is broken, you will see it immediately.

import org.junit.jupiter.api.Test; 

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void publicArticlesAreAccessibleForAnonymous() throws Exception {
    // An anonymous request to the public zone should pass
    mockMvc.perform(get("/api/public/articles"))
            .andExpect(status().isOk());
}

The second test is taken from a private zone that must require authentication. In our project, the REST semantics are configured so that anonymous users should receive 401, not a redirect to an HTML login page.

import org.junit.jupiter.api.Test; 

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void meEndpointRequiresAuthentication() throws Exception {
    // Without authentication, the private endpoint should return 401 Unauthorized
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isUnauthorized());
}

These checks do not yet describe the whole access matrix. They answer one basic question: is security really participating in the test, or are you still running around the filters.

If the second test suddenly returns 200, that is a very strong signal: either your filters are not connected, or /api/me somehow became public, which is almost certainly an error for our access matrix. And that is exactly why these two tests are useful to write immediately after setting up the environment, before anything “interesting”.

5. Auto-configuration vs manual assembly: how to choose

When a beginner developer sees two paths, they often take a third: they grab both and mix them “just in case”. In tests this is especially dangerous: you can end up in a situation where some tests go through one MockMvc, others through another where security is connected differently, and then you spend time asking “why do identical requests behave differently?”

To keep your brain from boiling, it is convenient to compare the approaches in one table:

Approach What it gives you What to remember
@SpringBootTest + @AutoConfigureMockMvc Quick start, MockMvc as a bean, high similarity to runtime The context is full, tests may be a bit heavier, but that is fine for a regression pack
webAppContextSetup(context) .apply(springSecurity()) Full control over MockMvc assembly, custom settings can be added It is easy to forget springSecurity() and get “tests without security”

The logic here is simple. If you have no reason to assemble things manually, do not. Write basic tests with auto-configuration, and let your energy go to the substance, that is, access checks, not to engineering archaeology: “why did the filters not work here?”

And one more thing worth saying out loud. Sometimes developers, with the best intentions, disable filters to “speed up tests.” In @AutoConfigureMockMvc, this is done with one parameter... and that is the “make security tests meaningless” button. If you are writing security tests, you cannot disable filters, otherwise you are literally testing the application in a parallel universe.

6. Typical mistakes when connecting MockMvc to SecurityFilterChain

The nasty thing about mistakes in this topic is that they often look like “well, the tests are green.” Yes, green. They just do not check the right thing. So below are the most common traps that are better avoided while they are still lying on the path and have not yet hit you in the forehead.

Mistake #1: build MockMvc with standaloneSetup and expect security to work.
standaloneSetup is good when you are testing a controller as an isolated object and want to verify mapping, parameters, and serialization. But it does not promise you a real filter infrastructure. In security tests, this leads to the effect of “the controller responds, so access is allowed”, although in reality access is decided before the controller.

Mistake #2: forget the spring-security-test dependency.
This sounds banal, but it happens all the time: you try to call .apply(springSecurity()), and it is as if the method does not exist. Or you want to use future tools like user() and @WithMockUser, and the IDE looks at you as if to say, “what is that supposed to be?” Usually the problem is solved by one line in dependencies, but until you add it, you are truly trying to cook borscht without a pot.

Mistake #3: assemble MockMvc manually but do not apply .apply(springSecurity()).
Visually, the tests may keep running, especially on public endpoints. That is why the mistake is so tricky: you write a test “public OK”, it is green, and you are happy. Then you write a test “/api/me returns 401” and get 200. You start suspecting the controller, the security config, the phase of the moon... when the real problem is that security was not involved in request processing at all.

Mistake #4: disable filters in @AutoConfigureMockMvc and not notice it.
Disabling filters is a popular “accelerator”, but in security tests it is like an “accelerator” for a car in the form of removed brakes: yes, faster, but the joy is questionable. If filters are disabled, requests do not pass through SecurityFilterChain, and you are not checking access rules. You are only checking that the controller can return responses.

Mistake #5: expect the wrong status because of inconsistent security semantics.
If your API has already been brought to REST-friendly behavior, then anonymous users on a private endpoint should receive 401, and an authenticated user without permissions should receive 403. If you suddenly see a 302 redirect to /login, that suggests you are testing browser semantics or using a configuration that returns an HTML redirect instead of a JSON error. The important thing is not to “force the test to fit the odd status”, but to understand what semantics your project should have.

1
Zadanie
Spring Security, poziom 26, lekcja 1
Niedostępne
Auto-configured MockMvc z rzeczywistym SecurityFilterChain
Auto-configured MockMvc z rzeczywistym SecurityFilterChain
1
Zadanie
Spring Security, poziom 26, lekcja 1
Niedostępne
Ręczne składanie MockMvc przez webAppContextSetup
Ręczne składanie MockMvc przez webAppContextSetup
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION