Spring Boot 测试最大的好处是切片测试:测 Controller 就只起 Web 层,连数据库都不碰;测 Repository 就只起 JPA 层。比 @SpringBootTest 全量启动快得多。
| 注解 | 测什么 |
@SpringBootTest | 全量启动容器,集成测试用。 |
@WebMvcTest | 只起 Spring MVC 层,测 Controller。配合 MockMvc 发请求。 |
@DataJpaTest | 只起 JPA 层,测 Repository,自动用内存数据库。 |
@MockBean | 把容器里某个 Bean 替换成 Mockito Mock,不真正调它。 |
用 MockMvc 测一个 Controller 接口
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(HelloController.class) // 只起 HelloController 这一个 Controller
class HelloControllerTest {
@Autowired
MockMvc mockMvc; // 模拟 HTTP 请求,不用真起 Tomcat
@Test
void helloShouldReturnGreeting() throws Exception {
mockMvc.perform(get("/hello").param("name", "豆包"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("豆包")));
}
}
用 @MockBean 替换真实依赖
@WebMvcTest(TaskController.class)
class TaskControllerTest {
@Autowired MockMvc mockMvc;
// TaskService 是真实接口,但这里用 Mockito 假的替换掉
@MockBean
TaskService taskService;
@Test
void listShouldReturnTasks() throws Exception {
// 打桩:调用 service.list(0, 10) 时返回固定数据
when(taskService.list(0, 10)).thenReturn(...);
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}
}
如果测试需要真数据库,可以用 Testcontainers——它在测试时自动拉起一个 Docker 容器跑 PostgreSQL/Redis,测完自动销毁。生产用什么数据库,测试就用什么,避免"本地 H2 跑得好好的,上 PostgreSQL 全是坑"。
@DataJpaTest:只起数据库层测 Repository
测 Repository 不用起整个 Web 容器。@DataJpaTest 只加载 JPA 相关 Bean,自动配一个内存数据库(H2),每个测试方法跑完自动回滚。
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.beans.factory.annotation.Autowired;
@DataJpaTest // 只起 JPA 层,不起 Web 层,秒级启动
class TaskRepositoryTest {
@Autowired TaskRepository repo;
@Autowired TestEntityManager em; // 测试专用的实体管理器
@Test
void findByDoneShouldFilter() {
em.persist(new Task(1, "买菜", false));
em.persist(new Task(2, "做饭", true));
List<Task> open = repo.findByDone(false);
assertEquals(1, open.size());
assertEquals("买菜", open.get(0).getTitle());
// 方法结束自动回滚,不污染下一个测试
}
}
@SpringBootTest:全量集成测试(慢但真实)
@SpringBootTest // 起完整容器,接近真实运行环境
class TaskServiceIntegrationTest {
@Autowired TaskService taskService;
@Test
void createShouldSaveToDb() {
Task t = taskService.create("集成测试任务");
assertNotNull(t.getId()); // 真的落库了
assertEquals("集成测试任务", t.getTitle());
}
}
三种测试的启动速度对比:
@DataJpaTest → 秒级(只起 JPA)
@WebMvcTest → 秒级(只起 MVC)
@SpringBootTest → 十秒级(全量起,含数据库、Redis...)
# 写单元/切片测试用前两个,写少数关键链路的端到端测试才用 @SpringBootTest。
Testcontainers:用真实数据库跑测试,不骗自己
@DataJpaTest 默认用 H2 内存库,但 H2 和你生产的 MySQL 语法、索引行为不完全一样——"测试通过、上线炸"。Testcontainers 在测试时自动起一个一次性的 MySQL 容器,测完就删,让测试用的库和生产一模一样。
@SpringBootTest
class UserRepositoryIT {
// 测试前自动起一个 MySQL 8 容器,测完自动销毁
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", mysql::getJdbcUrl);
r.add("spring.datasource.username", mysql::getUsername);
r.add("spring.datasource.password", mysql::getPassword);
}
// 之后用 @Autowired 注入 Repository,跑的就是真 MySQL
}
论什么时候值得上
测试启动要拉镜像、起容器,比 H2 慢几十秒。所以单元测试别用,留给少量关键的集成测试(IT)——尤其是写了复杂 SQL、依赖数据库方言的地方。CI 环境装了 Docker 就能跑。
本章面试题 · 第 9 章(测试)
1.(对比题)@SpringBootTest、@WebMvcTest、@DataJpaTest 分别什么时候用?
查看答案
答案:@WebMvcTest 只测 Controller(Service 用 @MockBean 替身);@DataJpaTest 只测 Repository(自动用内存库);@SpringBootTest 起全容器,测跨层集成。解析:原则:能切片不全量,切片快且隔离。
2.(理解题)@MockBean 和 @SpyBean 的区别?
查看答案
答案:@MockBean 整个 Bean 换成假的,方法调用要你 stub;@SpyBean 包装真实 Bean,默认调真方法,只拦截你指定的几个方法。解析:WebMvcTest 里替换 Service 用 @MockBean;想保留真实逻辑只改一处用 @SpyBean。