Spring BootMockMvc
Spring Boot

MockMvc

MockMvc tests Spring MVC controllers without starting a real HTTP server. It dispatches requests through the full DispatcherServlet filter chain — including security, validation, and serialisation — and provides a fluent assertion API for the response. Use @WebMvcTest for controller-slice tests or MockMvcBuilders.standaloneSetup() for unit-level controller tests. This entry covers setup, request building, response assertions, security testing, file upload, and JSON path assertions.

MockMvc Setup

@WebMvcTest loads the web layer only — controllers, filters, security, and argument resolvers. Declare @MockBean for every service dependency the controller injects. Alternatively, use MockMvcBuilders.standaloneSetup() to test a controller in complete isolation without any Spring context.
Java
// ── Option 1: @WebMvcTest — web slice (recommended) ─────────────────
@WebMvcTest(OrderController.class)
@ActiveProfiles("test")
class OrderControllerWebMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    // Mock every dependency OrderController uses
    @MockBean private OrderService    orderService;
    @MockBean private JwtService      jwtService;
    @MockBean private CustomUserDetailsService userDetailsService;

    // ── Stub JwtService so @WebMvcTest security works ─────────────────
    @BeforeEach
    void stubSecurity() {
        when(jwtService.extractSubject(anyString()))
            .thenReturn("alice@example.com");
        when(jwtService.isValid(anyString(), any()))
            .thenReturn(true);
        when(userDetailsService.loadUserByUsername(anyString()))
            .thenReturn(buildPrincipal("alice@example.com", "USER"));
    }
}

// ── Option 2: Standalone setup — no Spring context ────────────────────
class OrderControllerStandaloneTest {

    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        OrderService orderService = mock(OrderService.class);
        OrderController controller =
            new OrderController(orderService);

        mockMvc = MockMvcBuilders
            .standaloneSetup(controller)
            .setControllerAdvice(new GlobalExceptionHandler())
            .addFilters(new CorrelationIdFilter())
            .build();
    }
}

// ── Option 3: Full context MockMvc ────────────────────────────────────
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class OrderFullContextTest {

    @Autowired
    private MockMvc mockMvc;

    // Full context — real services and database
}

Building Requests and Asserting Responses

MockMvc's perform() dispatches a request through the DispatcherServlet. Chain andExpect() calls to assert status, headers, content type, and body. andDo(print()) logs the full request and response for debugging. andReturn() gives access to the raw MvcResult for custom assertions.
Java
@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired  private MockMvc      mockMvc;
    @Autowired  private ObjectMapper objectMapper;
    @MockBean   private ProductService productService;

    @Test
    @WithMockUser(roles = "USER")
    void findAll_returns200_withPagedProducts()
            throws Exception {
        Page<ProductResponse> page = buildPage(
            List.of(buildProductResponse(1L, "Widget"),
                    buildProductResponse(2L, "Gadget")));
        when(productService.findAll(any(Pageable.class)))
            .thenReturn(page);

        mockMvc.perform(
            get("/api/v1/products")
                .param("page", "0")
                .param("size", "20")
                .param("sort", "name,asc")
                .accept(MediaType.APPLICATION_JSON))

            // ── Status ───────────────────────────────────────────────
            .andExpect(status().isOk())

            // ── Headers ──────────────────────────────────────────────
            .andExpect(content().contentType(
                MediaType.APPLICATION_JSON))

            // ── JSON body — JSONPath ──────────────────────────────────
            .andExpect(jsonPath("$.content").isArray())
            .andExpect(jsonPath("$.content.length()").value(2))
            .andExpect(jsonPath("$.content[0].name")
                .value("Widget"))
            .andExpect(jsonPath("$.totalElements").value(2))
            .andExpect(jsonPath("$.page").value(0))

            // ── Debug — print request and response ────────────────────
            .andDo(print());
    }

    @Test
    @WithMockUser(roles = "USER")
    void create_returns201_withLocationHeader()
            throws Exception {
        ProductResponse created = buildProductResponse(42L, "Widget");
        when(productService.create(any()))
            .thenReturn(created);

        String requestBody = objectMapper.writeValueAsString(
            new CreateProductRequest("Widget",
                new BigDecimal("9.99"), "electronics"));

        MvcResult result = mockMvc.perform(
            post("/api/v1/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))

            .andExpect(status().isCreated())
            .andExpect(header().exists("Location"))
            .andExpect(header().string("Location",
                containsString("/api/v1/products/42")))
            .andExpect(jsonPath("$.id").value(42))
            .andExpect(jsonPath("$.name").value("Widget"))
            .andReturn();

        // Access raw response if needed
        String body = result.getResponse()
            .getContentAsString();
        ProductResponse response = objectMapper.readValue(
            body, ProductResponse.class);
        assertThat(response.id()).isEqualTo(42L);
    }

    @Test
    @WithMockUser(roles = "USER")
    void create_returns400_whenNameIsBlank()
            throws Exception {
        String invalid = objectMapper.writeValueAsString(
            new CreateProductRequest("",
                new BigDecimal("9.99"), "electronics"));

        mockMvc.perform(
            post("/api/v1/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalid))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.fieldErrors.name")
                .isNotEmpty());
    }
}

Security Testing with MockMvc

@WebMvcTest loads the SecurityFilterChain. Test authentication and authorisation directly in MockMvc tests using @WithMockUser, @WithUserDetails, or by crafting a real JWT and sending it in the Authorization header. Test both the happy path and the security rejection scenarios.
Java
@WebMvcTest(OrderController.class)
class OrderControllerSecurityTest {

    @Autowired  private MockMvc      mockMvc;
    @Autowired  private ObjectMapper objectMapper;
    @MockBean   private OrderService orderService;
    @MockBean   private JwtService   jwtService;
    @MockBean   private CustomUserDetailsService userDetailsService;

    // ── @WithMockUser — simplest, populates SecurityContext ───────────
    @Test
    @WithMockUser(username = "alice@example.com", roles = "USER")
    void findById_returns200_forAuthenticatedUser()
            throws Exception {
        when(orderService.findById(eq(1L), any()))
            .thenReturn(buildOrderResponse(1L));

        mockMvc.perform(get("/api/v1/orders/1"))
            .andExpect(status().isOk());
    }

    // ── Without authentication — 401 ─────────────────────────────────
    @Test
    void findById_returns401_withoutAuth() throws Exception {
        mockMvc.perform(get("/api/v1/orders/1"))
            .andExpect(status().isUnauthorized());
    }

    // ── Wrong role — 403 ─────────────────────────────────────────────
    @Test
    @WithMockUser(roles = "USER")
    void adminEndpoint_returns403_forRegularUser()
            throws Exception {
        mockMvc.perform(
            delete("/api/v1/admin/users/1"))
            .andExpect(status().isForbidden());
    }

    // ── @WithUserDetails — loads from UserDetailsService ─────────────
    @Test
    @WithUserDetails(value = "admin@example.com",
                     userDetailsServiceBeanName =
                         "customUserDetailsService")
    void adminEndpoint_returns200_forAdminUser()
            throws Exception {
        mockMvc.perform(get("/api/v1/admin/stats"))
            .andExpect(status().isOk());
    }

    // ── Real JWT in Authorization header ─────────────────────────────
    @Test
    void findById_returns200_withValidJwt()
            throws Exception {
        String email = "alice@example.com";
        String jwt   = "valid.jwt.token";

        when(jwtService.extractSubject(jwt))
            .thenReturn(email);
        when(jwtService.isValid(eq(jwt), any()))
            .thenReturn(true);
        when(userDetailsService.loadUserByUsername(email))
            .thenReturn(buildPrincipal(email, "USER"));
        when(orderService.findById(eq(1L), any()))
            .thenReturn(buildOrderResponse(1L));

        mockMvc.perform(
            get("/api/v1/orders/1")
                .header(HttpHeaders.AUTHORIZATION,
                    "Bearer " + jwt))
            .andExpect(status().isOk());
    }

    // ── Custom SecurityMockMvcRequestPostProcessor ─────────────────────
    @Test
    void withCustomPrincipal() throws Exception {
        AppUser principal = buildAppUser(1L, "alice@example.com");
        when(orderService.findById(eq(1L), any()))
            .thenReturn(buildOrderResponse(1L));

        mockMvc.perform(
            get("/api/v1/orders/1")
                .with(user(principal)))
            .andExpect(status().isOk());
    }
}

File Upload and Multipart Requests

Test file upload endpoints with MockMultipartFile and multipart(). Verify the controller receives the file and metadata, delegates to the service, and returns the correct response. Test both valid uploads and rejections for oversized or wrong-type files.
Java
@WebMvcTest(FileController.class)
class FileControllerTest {

    @Autowired  private MockMvc      mockMvc;
    @MockBean   private FileService  fileService;

    @Test
    @WithMockUser(roles = "USER")
    void upload_returns201_withFileMetadata()
            throws Exception {
        MockMultipartFile file = new MockMultipartFile(
            "file",                         // param name
            "document.pdf",                 // original filename
            MediaType.APPLICATION_PDF_VALUE,// content type
            "PDF content".getBytes()        // content
        );

        FileUploadResponse response = new FileUploadResponse(
            "file-uuid-123", "document.pdf",
            "application/pdf", 12L,
            "/api/v1/files/file-uuid-123",
            Instant.now());

        when(fileService.store(any(MultipartFile.class), any()))
            .thenReturn(response);

        mockMvc.perform(
            multipart("/api/v1/files")
                .file(file)
                .param("description", "My document"))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.fileId")
                .value("file-uuid-123"))
            .andExpect(jsonPath("$.filename")
                .value("document.pdf"))
            .andExpect(jsonPath("$.url").isNotEmpty());
    }

    @Test
    @WithMockUser(roles = "USER")
    void upload_withMultipleFiles() throws Exception {
        MockMultipartFile file1 = new MockMultipartFile(
            "files", "photo1.jpg",
            MediaType.IMAGE_JPEG_VALUE,
            "JPEG data".getBytes());
        MockMultipartFile file2 = new MockMultipartFile(
            "files", "photo2.jpg",
            MediaType.IMAGE_JPEG_VALUE,
            "JPEG data 2".getBytes());

        when(fileService.store(any(), any()))
            .thenReturn(buildFileResponse("uuid1"))
            .thenReturn(buildFileResponse("uuid2"));

        mockMvc.perform(
            multipart("/api/v1/files/batch")
                .file(file1)
                .file(file2))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.length()").value(2));
    }

    @Test
    @WithMockUser(roles = "USER")
    void upload_returns400_forEmptyFile()
            throws Exception {
        MockMultipartFile empty = new MockMultipartFile(
            "file", "empty.txt",
            MediaType.TEXT_PLAIN_VALUE,
            new byte[0]);   // empty content

        mockMvc.perform(
            multipart("/api/v1/files").file(empty))
            .andExpect(status().isBadRequest());
    }
}

JSON Path and Response Body Assertions

MockMvc supports JSONPath expressions for deep assertions into response bodies. For complex assertions, deserialise the response body using ObjectMapper and use AssertJ. Use ResultMatcher composition to build reusable assertion blocks.
Java
@WebMvcTest(UserController.class)
class UserControllerJsonTest {

    @Autowired  private MockMvc      mockMvc;
    @Autowired  private ObjectMapper objectMapper;
    @MockBean   private UserService  userService;

    @Test
    @WithMockUser(roles = "ADMIN")
    void findAll_returnsPageWithCorrectStructure()
            throws Exception {
        when(userService.findAll(any()))
            .thenReturn(buildUserPage());

        mockMvc.perform(get("/api/v1/users"))

            // ── Existence checks ──────────────────────────────────────
            .andExpect(jsonPath("$.content").exists())
            .andExpect(jsonPath("$.totalElements").exists())

            // ── Type checks ───────────────────────────────────────────
            .andExpect(jsonPath("$.content").isArray())
            .andExpect(jsonPath("$.totalPages").isNumber())
            .andExpect(jsonPath("$.first").isBoolean())

            // ── Value checks ──────────────────────────────────────────
            .andExpect(jsonPath("$.content[0].id").value(1))
            .andExpect(jsonPath("$.content[0].email")
                .value("alice@example.com"))
            .andExpect(jsonPath("$.content.length()").value(3))

            // ── Contains checks ───────────────────────────────────────
            .andExpect(jsonPath("$.content[0].roles[*]",
                hasItem("USER")))

            // ── Absence checks — sensitive fields not exposed ──────────
            .andExpect(jsonPath("$.content[0].password")
                .doesNotExist())
            .andExpect(jsonPath("$.content[0].passwordHash")
                .doesNotExist());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void findById_responseDeserializesCorrectly()
            throws Exception {
        when(userService.findById(1L))
            .thenReturn(buildUserResponse(1L));

        String body = mockMvc.perform(
            get("/api/v1/users/1"))
            .andExpect(status().isOk())
            .andReturn()
            .getResponse()
            .getContentAsString();

        // Deserialise and use AssertJ for fluent assertions
        UserResponse response = objectMapper.readValue(
            body, UserResponse.class);

        assertThat(response.id()).isEqualTo(1L);
        assertThat(response.email())
            .isEqualTo("alice@example.com");
        assertThat(response.roles())
            .containsExactlyInAnyOrder("USER");
        assertThat(response.createdAt()).isNotNull();
    }

    // ── Reusable ResultMatcher ─────────────────────────────────────────
    static ResultMatcher isPageResponse() {
        return ResultMatcher.matchAll(
            jsonPath("$.content").isArray(),
            jsonPath("$.page").isNumber(),
            jsonPath("$.size").isNumber(),
            jsonPath("$.totalElements").isNumber(),
            jsonPath("$.totalPages").isNumber(),
            jsonPath("$.first").isBoolean(),
            jsonPath("$.last").isBoolean()
        );
    }

    @Test
    @WithMockUser
    void findAll_hasPageStructure() throws Exception {
        when(userService.findAll(any())).thenReturn(buildUserPage());
        mockMvc.perform(get("/api/v1/users"))
            .andExpect(status().isOk())
            .andExpect(isPageResponse());   // reusable matcher
    }
}