1. Full-context web test without a server
If you already know how to write @WebMvcTest and know how to start @SpringBootTest, the logical question is: “Why invent yet another mode?”. The answer is very practical: sometimes an application breaks not inside the controller, not inside the service, and not inside the repository, but at the seam — where everything works fine separately, yet together it suddenly becomes 500 and a sad developer.
From the @SpringBootTest modes, we need exactly MOCK here: slices are too narrow for such failures, and we do not need a real network round-trip yet. That is, we take the full ApplicationContext, but still stay inside the JVM.
Imagine ContentHub as an assembly line: a request comes into the controller, then goes to the service, then to the repository, then back, and somewhere along the way validation, serialization, @ControllerAdvice, filters, and, if enabled, the security chain are involved. In @WebMvcTest, we check only the beginning of the line and very carefully imitate what comes next. In a unit test, we check a piece of the line without the belt and motor — “we turned the gear by hand and made sure it rotates”.
But the combination @SpringBootTest + @AutoConfigureMockMvc is the “assembly line fully built, motor connected, but we run it in a test workshop without real city delivery” mode. In other words, we do not start the network, we do not listen on ports, but inside the JVM we run the request through the real application assembly. This is ideal when you want to verify the layering itself: wiring, configuration, errors, the JSON contract at the seam, and work with real data.
2. @SpringBootTest and the MOCK mode
Before we write a single line of MockMvc, it is worth pausing and saying honestly: “What does ‘full context’ mean?”. @SpringBootTest is not just “the annotation that starts Spring”. It brings up almost the entire application the way it would start in production: auto-configuration, your @Configuration, services, repositories, Jackson settings, Bean Validation, @ControllerAdvice, and so on.
@SpringBootTest has web environment startup modes (webEnvironment): NONE, MOCK, RANDOM_PORT, DEFINED_PORT. We need MOCK here because it starts the web layer without a real network server.
To put it as plainly as possible, MOCK is “Spring MVC inside the test”, not “Tomcat/Jetty/Netty outside”. The request will be represented by a MockHttpServletRequest object, and the response by MockHttpServletResponse. No TCP, no port, no “my firewall is blocking it”. Just you, Spring MVC, and your logic.
A mini scaffold to fix the idea in code:
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class ContentHubMockWebEnvironmentTest {
// Important: the application's full ApplicationContext will be started.
// And additionally — a mock web environment (without a real network server and ports).
}
And one more practical point: “full context” means that if your application has a database, migrations, and repositories, they will really participate. Unlike @WebMvcTest, where the data layer usually is not started at all. That is why in full-context tests, the topic of predictable data and a test profile stops being “beautiful architecture” and becomes a matter of survival.
@AutoConfigureMockMvc and MockMvc
Now let us move to the main character of the day: MockMvc. The @SpringBootTest annotation by itself does not have to give you MockMvc as a ready-to-use tool. It starts the context, but it does not promise that you will immediately get a convenient “HTTP control panel”. It is exactly @AutoConfigureMockMvc that tells Spring Boot: “Since this is a web application, configure MockMvc and put it into the context so that I can inject it”.
Psychologically, this is an important step. We are as if saying: “I want an integration test, but I want to interact with it at the HTTP boundary level.” And then we get a nice compromise: the test runs as an integration test (real beans, real layer assembly), but is written like an MVC test (URI, status, headers, JSON). For a junior developer, this is very comfortable because the syntax is familiar from the @WebMvcTest days.
A minimal class scaffold looks like this:
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // Start the full application context.
@AutoConfigureMockMvc // Ask Spring Boot to configure MockMvc and put it into the context.
class PublicArticleFullContextTest {
}
And this is how we get MockMvc from the context:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
class PublicArticleFullContextTest {
@Autowired
MockMvc mockMvc; // "HTTP control panel": executes requests through Spring MVC without starting a server or network.
}
If your project has an AssertJ-friendly setup, then MockMvcTester may also live nearby. It is pleasant to use when you want to write assertions in a more “fluent” style, but in essence it is the same HTTP control panel.
3. Full-context test scaffold
Now comes the most important (and the simplest) moment of the lecture: let us make a test that really performs an HTTP request, but does so in a full context and without a real server. It is easy to fall into the trap of “I understood the theory, but I am afraid to write the code”. No need to be afraid: the syntax is almost the same as in @WebMvcTest, only the scale is different under the hood now.
For example, let us take a public ContentHub endpoint: reading an article by slug. In the ideal world, we already have migrations, a test profile, and test data can be prepared in advance. To avoid overcomplicating things, for now we will make a request with a “non-existent slug” and check that the correct error in the ApiProblem format (or an equivalent error contract) is returned. This is a convenient start because it does not require preloading articles into the database — we are checking a negative scenario that should always work.
Test scaffold:
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // Full application context (the service and data layers will also be started).
@AutoConfigureMockMvc // Add MockMvc to the context so the test can perform HTTP requests.
class PublicArticleFullContextTest {
}
Now let us add one test method. Notice: here we are already “speaking HTTP”, but we have a full context, which means errors should pass through the real @ControllerAdvice, be serialized by the real ObjectMapper, and all of this together should produce a stable JSON response.
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 returns404ForMissingSlug() throws Exception {
// Perform the request the way a client would: GET the resource URI.
mockMvc.perform(get("/api/public/articles/missing-slug"))
// Check only the HTTP status: 404 — the resource was not found.
.andExpect(status().isNotFound());
}
And if we want to verify not just the status, but also the error contract, let us add jsonPath. This is where the advantage of the mode becomes visible: you check the whole chain “resource not found” → “a domain exception was thrown” → “translated into ApiProblem” → “returned as JSON”.
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void returnsProblemDetailsForMissingSlug() throws Exception {
mockMvc.perform(get("/api/public/articles/missing-slug"))
.andExpect(status().isNotFound())
// Check that the error contract really returned the expected error code.
.andExpect(jsonPath("$.errorCode").value("ARTICLE_NOT_FOUND"));
}
If you are already used to writing accept(MediaType.APPLICATION_JSON), you can and should do that too, especially when you are fixing an API contract. In full-context mode, this additionally protects you from strange default behaviour.
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void returnsJsonForPublicEndpoint() throws Exception {
mockMvc.perform(get("/api/public/articles/missing-slug")
// Explicitly say: "I expect JSON" to fix the contract.
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
And one last small but important note: because this is a full context, you can inject not only MockMvc, but also, for example, a repository to check the data state. This is closer to “expensive” tests with high ROI — we are only pointing out the possibility here, so you do not think that a full-context test is “just another MVC test”.
4. Differences from @WebMvcTest
It is very easy to draw the wrong conclusion: “Well, I wrote MockMvc before, and I write it now. So it is the same thing, just with different annotations.” Unfortunately (or fortunately), no. The main difference is not in syntax, but in which world is behind your request and what a green test means. In @WebMvcTest, the world is small and intentionally “trimmed”. In @SpringBootTest + @AutoConfigureMockMvc, the world is real — as much as possible without the network.
To fix the differences without philosophy, it is convenient to compare the modes in a table:
| Characteristic | @WebMvcTest | @SpringBootTest + @AutoConfigureMockMvc |
|---|---|---|
| Which context is started | Narrow MVC slice (usually 1 controller + MVC infrastructure) | Full ApplicationContext of the application |
| Typical controller dependencies | Often mocked (@MockitoBean/@MockBean) | Usually real services/repositories/configs |
| Database | Usually does not participate | May participate (depending on configuration) |
| Startup cost | Low / medium | High |
| What it is especially good at catching | HTTP contract errors in the controller | Layer-assembly and configuration failures, seam errors |
| What it does not want to do | Check business logic and persistence | Duplicate what is already covered by unit tests and slices |
This distinction helps you avoid a very common trap. If you move all your @WebMvcTest tests into full-context mode, you will almost certainly end up with a slow suite and minimal additional value. If, on the other hand, you try to catch a layer-assembly failure only with @WebMvcTest, you will be “inventing reality” with mocks — and at some point the test will be green, while production will be red (usually on Friday evening, because bugs have a sense of humor).
Another important consequence: in a full-context test you usually mock less and control data and configuration more. And that makes sense: if the goal is to check the real layer assembly, then replacing half the layers with mocks is like checking the strength of a bridge by replacing half the spans with cardboard. Technically, you “checked something”, but the confidence in the result is questionable.
5. The proof boundary of the test
Here it is important to set the boundary right away. Full-context MockMvc proves very well that a request goes through the real MVC chain inside the full ApplicationContext: controller, services, repositories, @ControllerAdvice, Jackson, filters, and security. This is exactly the class of failures we pay for with the cost of the full context.
But there is no real network round-trip here: no port, no TCP connection, and no real HTTP client. Therefore, a green test means “the application is assembled correctly inside the JVM”, not “we have already checked the behaviour of the live server”. For this mode, that is enough: right now we care specifically about the internal assembly of the application.
6. Typical mistakes in full-context tests
At this point, you usually want to say, “All right, everything is clear, let us start writing tests in bulk.” And that is exactly where it is useful to slow down a bit and look at the mistakes developers most often make when they first move to full-context MockMvc. They are not difficult, they are tricky: you will not notice them until the suite grows and starts behaving like a capricious cat — “I run today, not tomorrow, and I will not explain why”.
Mistake #1: forgetting @AutoConfigureMockMvc and wondering why MockMvc is not injected.
@SpringBootTest starts the context, but it is not obliged to provide MockMvc automatically. As a result, you get a NoSuchBeanDefinitionException or simply cannot inject the tool. The fix is simple: for full-context web tests, “two annotations” are not decoration, but the minimum package.
Mistake #2: assuming that because the test is “HTTP-ish”, it is almost like a real server.
Intuitively it feels like: “I am doing GET/POST, so this is almost production.” In reality, no: this is Spring MVC in a mock environment. The HTTP semantics are real for Spring MVC here, but there is no real port, no client, and no network round-trip. This mode is excellent at catching failures inside the application, not the transport around it.
Mistake #3: carrying over the mental model of @WebMvcTest and mocking everything in sight.
In a full-context test, mocking half the application usually destroys the point of the mode. Yes, there are cases where you need to substitute an external adapter or a heavy integration, but that should be deliberate and targeted. If your full-context test looks like @WebMvcTest, only slower, then you are paying a high price for something you could get cheaply.
Mistake #4: not activating the test profile and accidentally testing “as in production”.
This mistake is especially painful when the configuration contains real addresses, real credentials, or real filesystem paths. In the course project, this is usually solved by the discipline of application-test.yml and @ActiveProfiles("test"). Full-context tests are powerful, but they should not “suddenly” go somewhere you did not plan to go.
Mistake #5: writing a test that checks only status().isOk().
A full context starts at a high cost. If you spend that cost just to check “well... 200, right?”, then you are buying a Ferrari to drive to the store next door for bread (and still spending 20 minutes looking for parking). In full-context tests, it is especially important to check what you are paying for: the error contract, serialization of important fields, the real interaction between layers, and seam behaviour.
GO TO FULL VERSION