1. The boundaries of @WebMvcTest
Separately from MVC, we have already locked down the DTO JSON contract. But even a perfectly serializable DTO is useless if the request never reached the right handler, the path/query parameters were not read, or the response came back with the wrong status. @WebMvcTest lifts us exactly to this layer: routing, binding, and the HTTP response around the controller.
When a person first sees Spring Boot, it seems that the most reliable test is the one where everything starts up “like in production.” In your head, some magic picture appears: “if the application started, then it works.” And then, suddenly, it turns out that “started” and “did not break on a real request” are different universes, sometimes even parallel ones.
@WebMvcTest exists precisely so that we can deliberately check the web boundary without paying for the rest of the world (in startup time and diagnostic complexity). A controller is not just “a method that calls a service.” It is the place where Spring MVC does a lot of work: URL routing, binding query/path parameters, type conversion, JSON reading, choosing encoding, building the response, and sometimes running validation. We want the test to “feel” this mechanism, but at the same time not start the database, migrations, and repositories, because that is a completely different risk area and a different test cost.
Think of a controller as an airport border officer. Its job is to check the passport (the HTTP request), understand where you are going (the route), correctly read your data (binding/JSON), and issue your boarding pass (the HTTP response). It is not supposed to repair the plane or check the engines — that is outside its responsibility. @WebMvcTest lets you test the “border officer,” not the whole airport.
2. The boundary of a controller test
The word slice in Spring Boot testing is not marketing and not “pseudo-architecture.” It is a very practical agreement: we start only the part of the ApplicationContext that is needed to verify a specific layer. In our case, that layer is Spring MVC and everything involved in processing an HTTP request before we fall into business logic.
The boundary of a controller test usually looks like this: we want to make sure the request reached the right handler method, that the input parameters were correctly read and converted, and that the expected status/body was returned. At the same time, we consciously do not prove in a controller test that “the service correctly publishes an article” or that “the repository correctly made a query to the database.” That is simply a different test level. If you try to prove everything at once, the test becomes expensive, fragile, and starts failing “everywhere and nowhere” at the same time — like that alarm clock that rings, but you do not know which sound you are supposed to wake up to.
A useful effect of the slice approach is very down to earth: when @WebMvcTest fails, the problem area usually stays within the web layer. And that is a huge time saver, especially for beginners who still cannot instantly identify the culprit from a 300-line stack trace (I cannot either, to be honest; I just pretend I can).
3. The composition of the MVC slice
When you write @WebMvcTest, Spring Boot assembles the test context so that request processing can run through Spring MVC without starting a real server. That means you get the full MVC infrastructure, but no Tomcat/Jetty/Netty as a live process on a port. This greatly speeds up the tests and makes them “local.”
Let us look at a typical ContentHub controller. It is exactly the target for an MVC slice test: a web boundary plus a dependency that goes farther out (into a service).
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// Web boundary: the controller is responsible only for the HTTP layer, not business logic
@RestController
@RequestMapping("/api/public/articles") // Base path for public endpoints
class PublicArticleController {
// Dependency beyond the MVC slice: in a controller test it is usually replaced
private final PublicArticleService publicArticleService;
// Constructor injection: Spring will pass the needed bean here (or the test will fail at startup)
PublicArticleController(PublicArticleService publicArticleService) {
this.publicArticleService = publicArticleService;
}
}
What matters here is the fact itself: the controller is a web component, @RestController. It is naturally “understood” by the MVC slice.
To make it easier to keep the picture in your head, it is useful to remember: @WebMvcTest starts everything needed for Spring MVC to process a request. This usually includes controllers, web-level exception handlers (@ControllerAdvice), ObjectMapper/message converters for JSON, converters/formatters for parameter binding, validation support (if you use Bean Validation), and the object through which the test will “send requests” — MockMvc.
Here is the minimal test skeleton that shows the idea itself: we start only the MVC slice and get MockMvc from the context.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(PublicArticleController.class) // Start the MVC slice only for the specified controller
class PublicArticleControllerWebMvcTest {
@Autowired
MockMvc mockMvc; // Tool for "requests" without starting a real server
@Test
void mvcInfrastructureIsAvailable() {
// At this point, the important thing is the fact itself:
// the MVC slice started and MockMvc is available.
//
// We could call mockMvc.perform(...) here,
// but in this example we are focusing specifically on the boundary and the context composition.
}
}
Notice that we are not “firing requests” yet. Right now it is more important to understand the boundary and the composition. Once this becomes clear, the perform (...) calls, status assertions, and JSON checks will no longer look like magic. But even now you can see the key point: MockMvc appears as part of the slice context, and that is not a coincidence, but a basic @WebMvcTest contract.
Below is a small table to fix the feeling of “who lives in this context and who does not.” This is not a legal document, but as a mental map it helps a lot.
| Inside @WebMvcTest (usually) | Why it is inside |
|---|---|
@Controller, |
This is the web boundary itself, the test target |
| @ControllerAdvice | This is part of the web contract: how errors turn into responses; sometimes it is convenient to add advice explicitly via @Import so the error layer is visible in setup |
| MVC infrastructure (DispatcherServlet, handler mapping, etc.) | Without this, the request will not “reach” the controller |
| JSON message converters and ObjectMapper | Needed to read/write JSON |
| Parameter converters/formatters | Needed for query/path parameters and their types |
| Bean Validation (if enabled) | Needed to validate the request DTO at the boundary |
| MockMvc | The tool that lets you run a request without a server |
This table matters not for memorization. It matters so that when a test does not start or behaves “strangely,” you do not think: “Spring is broken.” Most of the time it is not broken — it is honestly respecting the slice boundary.
4. Components outside @WebMvcTest
The most common emotional reaction of a beginner to @WebMvcTest sounds like this: “I added the annotation, and now it complains about a missing service. Why can Spring not just pull everything up?” And this is a good moment, because this is where engineering thinking matures: a test does not have to be “maximum.” A test has to be sufficient for the specific risk.
By default, @WebMvcTest does not start your @Service, @Repository, and other application @Component classes that are not directly related to MVC. It should not automatically drag in the data layer, run migrations, connect a datasource, and assemble the whole business world. If it did, it would become a “small @SpringBootTest,” and that breaks the idea of slice tests and kills their main value: speed and locality.
In practice, this shows up very simply. If the controller depends on a service and the service is not started, the test context will fail at startup. And that is a good, useful honesty: it tells you that the dependency lies outside the web slice.
For example, if you try to start PublicArticleController, but do not define where the context should get PublicArticleService from, you will get an error like:
NoSuchBeanDefinitionException:
No qualifying bean of type 'PublicArticleService' available
And here it is important to read the message correctly: this is not “oh no, everything is broken,” but “you started the web context, and the service is not part of it by default.” The point of a slice test is precisely that we decide ourselves what to do with what is outside. Sometimes we replace the dependency, sometimes we explicitly connect an additional web component, and sometimes we realize that the current check should live at another test level entirely.
One subtle point: @ConfigurationProperties (which you use for settings) also usually do not get picked up by @WebMvcTest automatically. And that makes sense: property binding is part of the general application wiring, not a mandatory part of pure MVC. Sometimes controllers need them (for example, attachment limits), but then they are added to the slice deliberately. “Deliberately” is the key word; it saves you dozens of minutes when you are trying to figure out why the test suddenly started loading half the application.
5. The request flow in @WebMvcTest
To avoid thinking of @WebMvcTest as some magical ritual, it is useful to see once what such a test actually checks. The important thing here is not the API details, but the responsibility boundary: web on the inside, business service on the outside (most often).
flowchart TD
%% Source: the test itself, which sends a request through MockMvc
T[Test] -->|MockMvc| MVC[Spring MVC infrastructure]
%% Spring MVC invokes the required handler method
MVC --> C[Controller method]
%% The service is almost always "outside" the slice and is replaced in the test
C --> S["Service (outside the slice)"]
%% The response is formed at the web boundary and returned outward
C --> R[HTTP Response]
In this diagram, MockMvc is like a “request simulator”: it creates a mock request, passes it through the MVC infrastructure (routing, binding, message conversion), invokes the controller method, and assembles a mock response. In most controller tests, the service is outside the boundary and is replaced with predictable behavior. But today's lecture is not about how exactly to replace it; today's lecture is about the fact that replacement is needed at all, because a slice is a deliberately reduced context.
At the ContentHub level, this means something very concrete: when we write PublicArticleControllerWebMvcTest, we will assume that the service has already “given” us ArticleDetailsResponse in the correct shape (we already locked that down with JSON tests). In the controller test, we want to verify that the endpoint /api/public/articles/{slug} really returns this DTO outward and correctly “wraps” it in an HTTP response. In other words, we protect the HTTP boundary contract rather than proving the business article publishing itself.
6. Diagnosing @WebMvcTest startup
The first couple of @WebMvcTest failures usually feel like: “I just wanted to test one controller, and now I need to understand half of Spring.” In reality, the reason is usually banal, and you can diagnose it by the exception type — without reading tea leaves and without saying, “let us add one more annotation, maybe it will help.”
If the test fails at context startup with NoSuchBeanDefinitionException, that almost always means the controller (or another web component you included) requires a dependency that is not part of the MVC slice. In other words, you ran into the boundary. This is a good moment to stop and ask yourself: “Does this dependency belong to the web layer, or is it business/infrastructure further out?” If it is a business service, then it should not be inside the slice by default. If it is truly a web component (for example, @ControllerAdvice or a converter), then perhaps you simply did not include it.
If the test starts but behaves unexpectedly (for example, some endpoint suddenly returns 404), then the problem is often not the service or the database, but web routing: an incorrect @RequestMapping, a wrong path, a missing controller in the slice, or a URL conflict. And this is exactly where @WebMvcTest is good: it forces you to make the web layer correct and deterministic first, without side effects from the other layers.
And finally, if you suddenly see strange effects from the web infrastructure (for example, filters that change behavior), that is also part of the honest picture: @WebMvcTest starts the web infrastructure. If there are additional web components in the project, they can affect processing. That is normal; it is just important to remember that we are testing the boundary, and the boundary is not only the controller method, but also the wrapper around it.
7. Typical mistakes when working with @WebMvcTest boundaries
Mistake No. 1: treating @WebMvcTest as a “nearly full context.”
It is very tempting to think that if there is Spring in the test, then “almost everything is there.” But @WebMvcTest is exactly a slice, that is, a deliberately reduced context. If you treat it like a mini version of @SpringBootTest, you will constantly be surprised by missing beans and end up treating symptoms with “annotations on top of annotations.”
Mistake No. 2: trying to prove the business logic of the service in a controller test.
A controller test is a check of the web contract: route, input parameters, transformations, and response shape. If you start checking in it how exactly the service selects articles from the database or how policy allows status transitions, you are mixing layers. Such tests become fragile and poorly explain where the behavior broke.
Mistake No. 3: treating the absence of @Service/@Repository as a “Spring error.”
When @WebMvcTest fails because a service is missing, it is usually not a bug, but a signal about the boundary. That is exactly what slice tests exist for: they force you to explicitly decide what to do with external dependencies. If the context “pulled everything in by itself,” you would not notice extra dependencies and would quietly drift into expensive and hard-to-diagnose tests.
Mistake No. 4: expanding the slice “just in case” until it turns into half the application.
Sometimes you just want to “add some more configuration,” “import the whole config,” “let Spring figure it out.” Usually this ends with the test becoming slow and unexpectedly depending on things that are not related to the web boundary at all. A good slice is a strict diet, not a buffet.
Mistake No. 5: ignoring that the web boundary is infrastructure plus controller code.
Beginners often think: “I am testing a controller method.” In practice, you are testing much more: routing, binding, message conversion, and error handling at the web level. If you keep that in mind, it becomes clearer why @WebMvcTest is so useful and why it does not have to drag services, repositories, and databases along with it.
GO TO FULL VERSION