Controller tests: MockMvc
A service test does not check that the controller answers with the right code, that the JSON has the needed fields, or that a guest cannot create a post. For that you have to send HTTP requests. MockMvc does it without starting Tomcat: the request passes through all security filters, the controller and the error handlers, and you can check the response in code.
A dependency for security
To "log in" as a given user in tests, add:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-test</artifactId>
<scope>test</scope>
</dependency>The test class
// src/test/java/com/example/blog/PostControllerTests.java
package com.example.blog;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class PostControllerTests {
@Autowired
private MockMvc mvc;
@Autowired
private UserRepository users;
@BeforeEach
void createUser() {
users.save(new User("anna", "{noop}changeme"));
}
@Test
void returnsNotFoundForMissingPost() throws Exception {
mvc.perform(get("/api/posts/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.detail").value("Post 999 not found"));
}
@Test
void rejectsAnonymousPost() throws Exception {
mvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title": "Hello", "body": "Text"}
"""))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "anna")
void createsPost() throws Exception {
mvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title": "Hello", "body": "Text"}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.title").value("Hello"))
.andExpect(jsonPath("$.author").value("anna"));
}
}@AutoConfigureMockMvc creates the MockMvc bean. In Spring Boot 4 this annotation lives in the org.springframework.boot.webmvc.test.autoconfigure package. perform sends a request, andExpect checks the response. jsonPath("$.title") takes a field from the JSON: $ is the root of the response.
Logging in within a test
@WithMockUser(username = "anna") runs the test as if Anna were already logged in. The password is not checked, but PostService looks the author up in the database — that is why @BeforeEach creates a real anna user.
Forms and CSRF
Site pages have CSRF switched on, so a form request must carry the token. In a test with(csrf()) adds it:
// src/test/java/com/example/blog/PageControllerTests.java
@Test
@WithMockUser(username = "anna")
void createsPostFromForm() throws Exception {
mvc.perform(post("/posts").param("title", "Hello").param("body", "Text").with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("/posts/*"));
}csrf() is imported from SecurityMockMvcRequestPostProcessors. param passes the form fields, and the check expects a redirect to the new post's page.
The result
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.368 s -- in com.example.blog.PageControllerTests
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 s -- in com.example.blog.PostControllerTestsCommon mistake
Without .with(csrf()) the same form request gets 403, even under @WithMockUser: the CSRF filter rejects the request before login and the controller come into play. If a form test unexpectedly fails with Status expected:<302> but was:<403>, check the token first. The /api/** addresses do not need it — CSRF is off there.