Spring Boot 入门 Day 5:集成测试 + 打包部署,项目收工上线
·
Spring Boot 入门 Day 5:集成测试 + 打包部署,项目收工上线
写完代码只是第一步,测过、打包、能跑才叫真的搞定了。
今天要解决什么问题?
前 4 天我们写出了完整的 CRUD API,但你有没有想过:
- 你怎么保证"改了一行代码,另一个功能没坏"?
- 每次都手动用
Invoke-WebRequest一条条测?不累吗? - 代码写完了怎么给别人用?发源码让人装 Maven?
今天一次性解决这三个问题:自动化测试 + 可执行 jar 包。
核心概念:测试金字塔
/\
/E2E\ ← 少:端到端测试(启动整个系统,模拟真实用户行为)
/------\
/集成测试\ ← 中:MockMvc 测试整个请求链路
/----------\
/ 单元测试 \ ← 多:测单个方法,快且低成本
/--------------\
| 测试类型 | 测什么 | 速度 | 我们今天做 |
|---|---|---|---|
| 单元测试 | 一个方法/类(依赖全 mock 掉) | 极快 | ❌ |
| 集成测试 | 从 HTTP 请求 → Controller → Service → 数据库全链路 | 中 | ✅ |
| E2E 测试 | 启动真实浏览器,模拟用户点击 | 慢 | ❌ |
今天我们写的是集成测试——性价比最高的一层。
MockMvc 是什么?
常规测试思维:启动项目 → 开浏览器 → 用 Postman 发请求 → 看结果
真实测试:
你的电脑 → HTTP 请求 → 8080 端口 → Tomcat → Spring → 返回 JSON
缺点:慢、要手动发请求、要人工判断结果
MockMvc 的做法:不走网络,直接发给 Spring 内部
MockMvc 测试:
测试代码 → 模拟 HTTP 请求 → 直接交给 Spring MVC 处理 → 返回结果
优点:不占端口、自动断言结果、速度快、可重复跑
MockMvc = 假装自己是 HTTP 客户端,直接跟 Spring MVC 对话。全链路都走了,就是不走网线。
动手写测试(7 个用例)
测试文件结构
@SpringBootTest // 1. 启动完整 Spring 上下文(含数据库)
@AutoConfigureMockMvc // 2. 自动配置 MockMvc
class TaskControllerTest {
@Autowired
private MockMvc mockMvc; // 3. 模拟 HTTP 请求的工具
@Autowired
private ObjectMapper objectMapper; // 4. Java对象 ↔ JSON 互转
}
关键注解解析
| 注解 | 作用 |
|---|---|
@SpringBootTest |
启动完整的 Spring 容器(包括数据库),是集成测试的标志 |
@AutoConfigureMockMvc |
自动创建 MockMvc 实例并注入 |
@Test |
标记这是一个测试方法 |
@BeforeEach |
每个测试方法执行前先跑,保证测试互相独立 |
测试 1:正常创建任务(应返回 201)
@Test
void shouldCreateTask() throws Exception {
// 准备数据
CreateTaskRequest req = new CreateTaskRequest();
req.setTitle("测试任务");
req.setPriority(Priority.HIGH);
// 发请求 + 断言结果
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))) // DTO → JSON
.andExpect(status().isCreated()) // 期望返回 201
.andExpect(jsonPath("$.title").value("测试任务")) // 期望 title 正确
.andExpect(jsonPath("$.status").value("TODO")) // 默认状态是 TODO
.andExpect(jsonPath("$.id").isNumber()); // 自动生成 id
}
一行行拆解:
mockMvc.perform(...) → 发送模拟 HTTP 请求
.andExpect(...) → 断言结果(链式调用,可以写多个)
status().isCreated() → HTTP 状态码必须是 201
jsonPath("$.title")... → 返回 JSON 的 title 字段必须等于 "测试任务"
jsonPath("$.id").isNumber() → id 字段必须是数字
jsonPath 语法速记: $ 表示整个 JSON 对象,$.field 取字段值。
测试 2:空标题 → 应返回 400
@Test
void shouldRejectEmptyTitle() throws Exception {
CreateTaskRequest req = new CreateTaskRequest();
req.setTitle(""); // 空字符串!
req.setPriority(Priority.MEDIUM);
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isBadRequest()) // 期望 400
.andExpect(jsonPath("$.code").value(400));
}
空标题 → @NotBlank 校验不通过 → MethodArgumentNotValidException
→ GlobalExceptionHandler 拦截 → 返回 { code: 400, message: "输入校验失败" }
这一步验证了校验和异常处理全局链。
测试 3:缺少必填字段 → 应返回 400
@Test
void shouldRejectMissingPriority() throws Exception {
CreateTaskRequest req = new CreateTaskRequest();
req.setTitle("无优先级");
// 不设 priority → @NotNull 校验失败
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isBadRequest());
}
测试 4:根据 ID 查询(先创建再查询)
@Test
void shouldReturnTaskById() throws Exception {
// 第一步:创建任务
CreateTaskRequest req = new CreateTaskRequest();
req.setTitle("Spring Boot");
req.setPriority(Priority.HIGH);
String response = mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andReturn().getResponse().getContentAsString(); // 拿到返回 JSON
int id = objectMapper.readTree(response).get("id").asInt(); // 提取 id
// 第二步:用 id 查询
mockMvc.perform(get("/api/tasks/" + id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Spring Boot"));
}
注意:测试里可以按顺序执行多个请求,这就是集成测试的优势——真实模拟用户操作流程。
测试 5:查询不存在的 ID → 应返回 404
@Test
void shouldReturn404WhenNotFound() throws Exception {
mockMvc.perform(get("/api/tasks/99999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(404));
}
这个测试最简单粗暴——直接查一个不存在的 ID,验证全局异常处理返回 404。
测试 6:更新任务(部分字段更新)
@Test
void shouldUpdateTask() throws Exception {
// 创建
CreateTaskRequest req = new CreateTaskRequest();
req.setTitle("原始标题");
req.setPriority(Priority.LOW);
String response = mockMvc.perform(post("/api/tasks")...)
.andReturn().getResponse().getContentAsString();
int id = objectMapper.readTree(response).get("id").asInt();
// 只更新标题
mockMvc.perform(put("/api/tasks/" + id)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"新标题\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("新标题")) // 标题变了
.andExpect(jsonPath("$.priority").value("LOW")); // 优先级没变!
}
验证了部分更新不覆盖其他字段的逻辑。
测试 7:删除任务(删完查不到)
@Test
void shouldDeleteTask() throws Exception {
// 创建
CreateTaskRequest req = new CreateTaskRequest();
req.setTitle("待删除");
req.setPriority(Priority.MEDIUM);
String response = mockMvc.perform(post("/api/tasks")...)
.andReturn().getResponse().getContentAsString();
int id = objectMapper.readTree(response).get("id").asInt();
// 删除
mockMvc.perform(delete("/api/tasks/" + id))
.andExpect(status().isNoContent()); // 期望 204
// 再查 → 应该 404
mockMvc.perform(get("/api/tasks/" + id))
.andExpect(status().isNotFound());
}
三步串联:创建 → 删 → 确认删干净了。
运行测试
cd F:/java.work/learning.txt/agent/task-manager-spring
mvn test
期望输出:
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
测试 vs 手动 curl 对比
| MockMvc 测试 | 手动 Invoke-WebRequest | |
|---|---|---|
| 重复执行 | mvn test 一键全跑 |
一条条手动复制粘贴 |
| 断言 | 自动判断结果对错 | 肉眼对比 JSON |
| 以后改代码 | 跑一次测试就知道有没有坏 | 可能漏测,上线才发现 |
| 速度 | 毫秒级 | 每次要等 HTTP |
测试的价值:你今天写完 7 个用例,以后每次改代码只需要 mvn test,3 秒告诉你有没有改坏。
打包部署:源码 → 可运行 jar
# 打包(跳过测试,因为测试依赖 @BeforeEach 清库端点)
mvn package -DskipTests
# 运行
java -jar target/task-manager-spring-1.0.0.jar
浏览器打开 http://localhost:8080/api/tasks,确认能正常访问。
为什么一个 jar 就能跑?Tomcat 在哪?
task-manager-spring-1.0.0.jar
├── BOOT-INF/
│ ├── classes/ ← 你的代码(编译后的 .class)
│ └── lib/ ← 所有依赖(Spring Boot、Hibernate、H2、Tomcat...)
├── META-INF/
└── org/springframework/boot/loader/ ← 启动加载器
Spring Boot 把 Tomcat 打包进了 jar 里(内嵌 Tomcat),所以:
- 不需要单独安装 Tomcat
- 不需要配置任何应用服务器
java -jar xxxx.jar一条命令搞定
等你换到生产环境,直接把 jar 拷到服务器,同样一条命令启动。
Day 5 总结
| 要点 | 一句话 |
|---|---|
| MockMvc | 不走网络但走完整 Spring MVC 链路,测全链路 |
@SpringBootTest |
启动完整容器(含数据库),是集成测试的标志 |
| 链式断言 | andExpect(jsonPath(...).value(...)) 语义清晰 |
mvn test |
一键跑完所有测试,3 秒验证代码质量 |
mvn package |
产出可执行 jar(内嵌 Tomcat + 全部依赖) |
java -jar |
一条命令部署,不用装任何东西 |
Week 1 完整回顾
5 天下来,你掌握的项目结构:
task-manager-spring/
├── pom.xml
└── src/
├── main/java/com/agent/
│ ├── Application.java ← 启动类
│ ├── controller/TaskController.java ← REST API(5 个端点)
│ ├── service/TaskService.java ← 业务 + 事务
│ ├── repository/TaskRepository.java ← JPA 接口,方法名即 SQL
│ ├── model/Task.java ← 数据库实体
│ ├── dto/ ← 请求/响应隔离
│ └── exception/ ← 全局异常处理
└── test/java/com/agent/controller/
└── TaskControllerTest.java ← 7 个集成测试
| 技能 | 掌握 |
|---|---|
| REST API | GET/POST/PUT/DELETE 完整实现 |
| 参数校验 | @Valid + Bean Validation + 统一错误格式 |
| JPA 持久化 | @Entity + JpaRepository 方法名自动生成 SQL |
| H2 数据库 | 文件模式重启不丢失 + Web Console 可视化 |
| 全局异常处理 | @RestControllerAdvice 拦截所有异常 |
| DTO 模式 | 请求/响应/实体三层分离 |
| 集成测试 | MockMvc 一键验证 7 个用例 |
| 打包部署 | java -jar 一条命令上线 |
一句话记住 Day 5:写完代码不算完,测试通过 + 打包能跑才算数。
Week 1 结束。下一阶段:认证授权、日志监控、Docker 容器化部署。
更多推荐




所有评论(0)