Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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.PostControllerTests

Common 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.

Thanks for reading!

Donate to the site — we work hard for you.