第 11 章 · 测试、安全与生产部署
本章目标:使用 @SpringBootTest 与 MockMvc 编写 Controller 层集成测试;配置 生产环境 安全项(HTTPS 反向代理、敏感配置外置);掌握 Spring Boot 可执行 jar 打包与 systemd 守护;完成 shop-spring-demo 上线前检查清单。
学时建议:4~5 小时(含 1.5 小时部署跟练)
前置:ch07~ch10;了解 Linux 基础命令与 SSH。
11.1 测试分层策略
┌─────────────────────────────────────────────┐
│ @SpringBootTest + MockMvc ← 本章重点 │
│ 启动完整上下文,模拟 HTTP,不启真实浏览器 │
├─────────────────────────────────────────────┤
│ @DataJpaTest ← Repository 层 │
├─────────────────────────────────────────────┤
│ 纯单元测试 @ExtendWith(MockitoExtension) │
└─────────────────────────────────────────────┘
| 类型 | 速度 | 覆盖 |
|---|---|---|
| 单元测试 | 快 | 单类逻辑 |
| MockMvc | 中 | Web 层 + Security 过滤链 |
| 全量 E2E | 慢 | 真实浏览器/TestRestTemplate |
教学项目 shop-spring-demo;测试库可用 H2 或 Testcontainers MySQL(选修)。
11.2 测试依赖与配置
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
src/test/resources/application-test.yml:
spring:
datasource:
url: jdbc:h2:mem:shop_test;MODE=MySQL
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
data:
redis:
host: 127.0.0.1
port: 6379
upload:
base-dir: ./uploads-test
11.3 @SpringBootTest 与 MockMvc
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class ProductApiTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setUp() {
productRepository.deleteAll();
Product p = new Product();
p.setName("测试商品");
p.setPrice(new BigDecimal("99.00"));
p.setPublished(true);
productRepository.save(p);
}
@Test
void listProducts_returnsOk() throws Exception {
mockMvc.perform(get("/api/products")
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.items[0].name").value("测试商品"));
}
}
| 注解 | 作用 |
|---|---|
@SpringBootTest | 加载完整 ApplicationContext |
@AutoConfigureMockMvc | 注入 MockMvc |
@ActiveProfiles("test") | 使用 test 配置 |
@Transactional(选修) | 测试后回滚 DB |
11.4 MockMvc 常用断言
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name":"新商品","price":19.9,"stock":100}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.name").value("新商品"));
校验失败 400:
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"price\":-1}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(40001));
| 方法 | 用途 |
|---|---|
get/post/put/delete | HTTP 方法 |
contentType / content | JSON 体 |
header / cookie | 认证头 |
andExpect(status()) | HTTP 状态 |
andExpect(jsonPath()) | JSON 字段 |
andDo(print()) | 调试打印响应 |
11.5 Spring Security 测试
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void createProduct_withAuth() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"权限商品\",\"price\":1,\"stock\":1}"))
.andExpect(status().isOk());
}
@Test
void createProduct_withoutAuth_forbidden() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isUnauthorized());
}
JWT 场景可用 SecurityMockMvcRequestPostProcessors.jwt():
mockMvc.perform(get("/api/orders/me")
.with(jwt().jwt(j -> j.subject("user1"))))
.andExpect(status().isOk());
11.6 生产配置要点
| 配置项 | dev | prod |
|---|---|---|
ddl-auto | update | validate / none |
show-sql | true | false |
| 日志级别 | DEBUG 包 | INFO/WARN |
| 密钥 | 本地占位 | 环境变量 |