SpringBoot Integration Testing: Using @SpringBootTest with MockMvc
SpringBoot integration tests load the full application context to verify end‑to‑end behavior, while unit tests only cover isolated methods; this guide explains @SpringBootTest fundamentals, configuration options, MockMvc usage, test structure, common pitfalls, and best practices for reliable full‑stack testing.
Why Integration Tests Matter
Unit tests that target a single service method can miss problems that appear only when the full request chain is exercised, such as controller parameter validation, interceptor execution, mapper return values, and global exception handling. Integration tests exercise the entire chain to catch these issues.
Integration Test vs Unit Test
Key differences:
Test Scope : Unit tests target a single class or method; integration tests cover the full chain (Controller → Service → Mapper → database or external API).
Spring Context : Unit tests do not load the context and rely on mocks; integration tests load the complete Spring context with real bean injection.
Core Tools : JUnit & Mockito for unit tests; @SpringBootTest, MockMvc, and optionally TestContainers for integration tests.
Speed : Unit tests run in milliseconds; integration tests take seconds because the context is started.
Applicable Scenarios : Unit tests verify isolated logic; integration tests validate full‑link behavior, configuration, and dependency interactions.
Environment Preparation
SpringBoot 2.2+ already includes spring-boot-starter-test (JUnit 5, MockMvc, Mockito). For older projects add it manually and keep the version aligned with the SpringBoot version. Optional dependencies for database or third‑party HTTP calls can be added as shown.
<!-- SpringBoot test core dependency (includes JUnit 5, MockMvc, Mockito) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Optional MySQL for DB tests -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>test</scope>
</dependency>
<!-- Optional OkHttp for external‑API simulation -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>A recommended test package layout mirrors the main source tree:
com.example.demo
├── DemoApplication.java // main class
├── controller
│ └── UserController.java
├── service
│ └── UserService.java
├── mapper
│ └── UserMapper.java
└── src/test/java
└── com.example.demo
├── controller
│ └── UserControllerTest.java
├── service
│ └── UserServiceTest.java
└── DemoApplicationTests.java // global entry point@SpringBootTest Deep Dive
@SpringBootTestis a context launcher. It scans for the class annotated with @SpringBootApplication, loads all configuration files (application.yml/properties, @Configuration classes, Bean definitions), and creates a complete Spring application context so that tests can inject real beans such as Service, Mapper, and Controller.
By default it does not start an embedded server unless the webEnvironment attribute is set.
Key Attributes
classes : Explicitly specify the startup class when multiple @SpringBootApplication classes exist. Example:
@SpringBootTest(classes = DemoApplication.class)
public class UserControllerTest {
// test code
}webEnvironment : Controls the web layer. WebEnvironment.MOCK (default) – mock servlet environment, suitable for MockMvc. WebEnvironment.RANDOM_PORT – starts a real server on a random port, useful for testing interceptors, CORS, etc. WebEnvironment.DEFINED_PORT – starts a real server on the port defined in configuration. WebEnvironment.NONE – no web environment, for pure service or mapper tests.
properties : Override configuration values for the test without touching application.yml. Example:
@SpringBootTest(properties = {
"spring.datasource.url=jdbc:mysql://localhost:3306/test_db",
"logging.level.com.example.demo=DEBUG"
})
public class ConfigOverrideTest {
// test code
}Basic Integration Example (Service Layer)
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserMapper userMapper;
@Test
public void testGetUserById() {
// 1. Insert test data
User testUser = new User(null, "测试用户", 25, "13800138000");
userMapper.insert(testUser);
Long userId = testUser.getId();
// 2. Call service
User resultUser = userService.getUserById(userId);
// 3. Verify
assertNotNull(resultUser);
assertEquals("测试用户", resultUser.getUsername());
assertEquals(25, resultUser.getAge());
// 4. Clean up
userMapper.deleteById(userId);
}
}No mocks are used; the test hits the real database (usually an in‑memory DB or a TestContainers instance).
MockMvc Advanced Usage
MockMvc simulates HTTP requests without starting a real server. It can test all HTTP verbs and verify status, headers, cookies, and JSON bodies.
Two Initialization Styles
Auto‑configuration (recommended): add @AutoConfigureMockMvc alongside @SpringBootTest and inject MockMvc directly.
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
// test methods …
}Manual construction : useful when custom interceptors, filters, or multipart handling are needed.
@SpringBootTest
public class CustomMockMvcTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext wac;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFileUpload(new MockMultipartFile("file", "test.txt", "text/plain", "test content".getBytes()))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
}
// test methods …
}Core Workflow
perform() : start a request, e.g. mockMvc.perform(get("/user/1")) or mockMvc.perform(post("/user/add")).
Configure request : add parameters, body, headers, cookies, etc.
// GET with path variable, query param, header, cookie
mockMvc.perform(get("/user/{id}", 1)
.param("type", "1")
.header("token", "test-token-123")
.cookie(new Cookie("userId", "123")));
// POST JSON body
mockMvc.perform(post("/user/add")
.contentType(APPLICATION_JSON)
.accept(APPLICATION_JSON)
.content("{\"username\":\"测试用户\",\"age\":25}"));
// POST form data
mockMvc.perform(post("/user/login")
.contentType(APPLICATION_FORM_URLENCODED)
.param("username", "admin")
.param("password", "123456"));
// File upload
mockMvc.perform(multipart("/user/upload")
.file(new MockMultipartFile("file", "test.jpg", "image/jpeg", "test image content".getBytes()))
.param("desc", "测试图片"));andExpect() : verify response status, headers, cookies, JSON fields, etc.
mockMvc.perform(get("/user/1"))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "application/json"))
.andExpect(cookie().value("userId", "123"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("测试用户"))
.andExpect(jsonPath("$.age").isNumber());Additional helpers: andDo(print()) prints request/response details; andReturn() gives access to the raw MvcResult for custom assertions.
Common Scenarios (90% Coverage)
GET with path & query parameters – verify pagination fields.
@Test
public void testUserList() throws Exception {
mockMvc.perform(get("/user/list")
.param("pageNum", "1")
.param("pageSize", "10")
.header("token", "test-token"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("success"))
.andExpect(jsonPath("$.data.list").isArray())
.andExpect(jsonPath("$.data.pageNum").value(1))
.andExpect(jsonPath("$.data.pageSize").value(10));
}POST JSON with success and validation error
@Test
public void testAddUser_Success() throws Exception {
User user = new User(null, "新用户", 22, "13800138000");
String json = new ObjectMapper().writeValueAsString(user);
mockMvc.perform(post("/user/add")
.contentType(APPLICATION_JSON)
.content(json))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("添加成功"));
}
@Test
public void testAddUser_Error_UsernameNull() throws Exception {
User user = new User(null, "", 22, "13800138000");
String json = new ObjectMapper().writeValueAsString(user);
mockMvc.perform(post("/user/add")
.contentType(APPLICATION_JSON)
.content(json))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.message").contains("username 不能为空"));
}Interceptor / token validation
@Test
public void testWithoutToken() throws Exception {
mockMvc.perform(get("/user/1"))
.andDo(print())
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401))
.andExpect(jsonPath("$.message").value("请先登录"));
}
@Test
public void testInvalidToken() throws Exception {
mockMvc.perform(get("/user/1").header("token", "invalid-token"))
.andDo(print())
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.message").value("token 无效"));
}File upload
@Test
public void testFileUpload() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "test.jpg", "image/jpeg", "test image content".getBytes());
mockMvc.perform(multipart("/user/upload").file(file).param("desc", "测试图片"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.filename").value("test.jpg"));
}Combining @SpringBootTest and MockMvc for Full‑Link Tests
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@AutoConfigureMockMvc
public class UserFullLinkIntegrationTest {
@Autowired private MockMvc mockMvc;
@Autowired private UserService userService;
@Autowired private UserMapper userMapper;
@Autowired private ObjectMapper objectMapper;
/** Full‑chain test: HTTP → Service → Mapper → DB */
@Test
public void testUserFullLink() throws Exception {
// 1. Insert test record
User testUser = new User(null, "全链路测试用户", 30, "13900139000");
userMapper.insert(testUser);
Long userId = testUser.getId();
// 2. MockMvc request
mockMvc.perform(get("/user/{id}", userId)
.header("token", "test-token-123"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.username").value("全链路测试用户"))
.andExpect(jsonPath("$.age").value(30));
// 3. Verify service bean works
User serviceUser = userService.getUserById(userId);
assertNotNull(serviceUser);
assertEquals("全链路测试用户", serviceUser.getUsername());
// 4. Clean up
userMapper.deleteById(userId);
}
}Test Data Isolation
Adding @Transactional to a test method rolls back all DB changes after the method finishes, keeping the database clean.
@Test
@Transactional
public void testWithTransaction() throws Exception {
User testUser = new User(null, "事务测试用户", 28, "13700137000");
userMapper.insert(testUser);
Long userId = testUser.getId();
mockMvc.perform(get("/user/{id}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("事务测试用户"));
// No explicit delete needed – transaction rolls back
} @Transactionalonly rolls back database operations; external calls must be mocked separately.
10 Common Pitfalls and Solutions
@SpringBootTest starts slowly – load only needed beans via a custom @TestConfiguration.
MockMvc 404 errors – check URL prefixes, ensure the controller package is scanned, and add @AutoConfigureMockMvc.
Bean injection failures – verify that the startup class is found and beans are annotated with @Service, @Controller, or @Repository.
Test data contaminates the DB – use @Transactional or TestContainers for an isolated temporary database.
File upload not simulated – use multipart() or build MockMvc manually with file‑upload support.
jsonPath validation errors – ensure the response is JSON and the path expression matches the actual structure.
@LocalServerPort injection fails – only works when webEnvironment is RANDOM_PORT or DEFINED_PORT.
Test execution order appears random – JUnit 5 runs tests in random order by default; use @Order if a specific sequence is required.
Unstable tests that depend on third‑party services – mock those services with Mockito or WireMock.
Mixing unit and integration tests in the same class – keep them separate (e.g., XxxServiceTest for unit tests, XxxControllerIntegrationTest for integration tests).
Making Integration Tests More Efficient
Static Imports
Import MockMvc request builders, result matchers, and handlers statically to avoid fully‑qualified class names.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
mockMvc.perform(get("/user/1")).andDo(print()).andExpect(status().isOk());Base Test Class
Extract common fields ( MockMvc, ObjectMapper) and utility methods into an abstract base class.
@SpringBootTest
@AutoConfigureMockMvc
public abstract class BaseIntegrationTest {
@Autowired protected MockMvc mockMvc;
@Autowired protected ObjectMapper objectMapper;
protected String toJson(Object obj) throws JsonProcessingException {
return objectMapper.writeValueAsString(obj);
}
}
public class UserControllerTest extends BaseIntegrationTest {
@Test
public void testUser() throws Exception {
User user = new User(null, "测试", 25);
mockMvc.perform(post("/user/add")
.contentType(APPLICATION_JSON)
.content(toJson(user)))
.andExpect(status().isOk());
}
}TestContainers for Temporary Databases
When local DB versions differ, spin up a disposable MySQL/PostgreSQL container for the test lifecycle. Add the TestContainers dependency and configure a @Container field; the container starts before tests and stops afterwards, guaranteeing environment consistency.
Conclusion
SpringBoot integration testing hinges on loading the full application context with @SpringBootTest and simulating HTTP calls via MockMvc. Together they provide end‑to‑end verification of Controller → Service → Mapper → database flows, catch configuration or interceptor issues early, and increase confidence before deployment. By following the patterns, handling common pitfalls, and applying the efficiency tips above, developers can write maintainable, fast, and reliable integration tests.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
