@llryiop/avatar-boot-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -0
- package/bin/cli.js +3 -0
- package/docs/plans/2026-03-12-avatar-boot-cli-design.md +73 -0
- package/docs/plans/2026-03-12-avatar-boot-cli-plan.md +681 -0
- package/package.json +28 -0
- package/src/index.js +78 -0
- package/src/prompts.js +78 -0
- package/src/template.js +37 -0
- package/src/transform.js +172 -0
- package/src/utils.js +34 -0
- package/templates/.claude/rules/architecture-redlines.md +146 -0
- package/templates/.claude/rules/code-review-standards.md +137 -0
- package/templates/.claude/rules/coding-standards.md +56 -0
- package/templates/.claude/rules/git-commit.md +59 -0
- package/templates/.claude/rules/layered-architecture.md +201 -0
- package/templates/.claude/rules/mybatis-plus.md +263 -0
- package/templates/.claude/rules/tech-stack.md +41 -0
- package/templates/.claude/rules/version.md +467 -0
- package/templates/.claude/settings.local.json +18 -0
- package/templates/.claude/skills/ai-tool-guide/SKILL.md +314 -0
- package/templates/.claude/skills/api-design/SKILL.md +200 -0
- package/templates/.claude/skills/api-doc-generator/SKILL.md +380 -0
- package/templates/.claude/skills/api-service-module-creator/SKILL.md +1114 -0
- package/templates/.claude/skills/avatar-boot-starter-feign/SKILL.md +243 -0
- package/templates/.claude/skills/avatar-boot-starter-job/SKILL.md +437 -0
- package/templates/.claude/skills/avatar-boot-starter-kafka/SKILL.md +580 -0
- package/templates/.claude/skills/avatar-boot-starter-mysql/SKILL.md +572 -0
- package/templates/.claude/skills/avatar-boot-starter-nacos/SKILL.md +901 -0
- package/templates/.claude/skills/avatar-boot-starter-oss/SKILL.md +594 -0
- package/templates/.claude/skills/avatar-boot-starter-redis/SKILL.md +586 -0
- package/templates/.claude/skills/avatar-boot-starter-rocketmq/SKILL.md +662 -0
- package/templates/.claude/skills/avatar-boot-starter-web/SKILL.md +1007 -0
- package/templates/.claude/skills/changelog-generator/SKILL.md +114 -0
- package/templates/.claude/skills/code-review/SKILL.md +239 -0
- package/templates/.claude/skills/crud-generator/SKILL.md +824 -0
- package/templates/.claude/skills/database-design/SKILL.md +377 -0
- package/templates/.claude/skills/deployment-config/SKILL.md +277 -0
- package/templates/.claude/skills/incident-analysis/SKILL.md +241 -0
- package/templates/.claude/skills/integration-test-generator/SKILL.md +496 -0
- package/templates/.claude/skills/prompt-engineering/SKILL.md +249 -0
- package/templates/.claude/skills/requirement-management/SKILL.md +244 -0
- package/templates/.claude/skills/security-audit/SKILL.md +330 -0
- package/templates/.claude/skills/test-case-design/SKILL.md +257 -0
- package/templates/.claude/skills/testing-workflow/SKILL.md +68 -0
- package/templates/.claude/skills/troubleshooting/SKILL.md +240 -0
- package/templates/CLAUDE.md +173 -0
- package/templates/README.md +303 -0
- package/templates/avatar-scaffold-api/pom.xml +41 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/api/LoginFeignClient.java +40 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/constant/LoginConstant.java +21 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/request/LoginRequest.java +17 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/request/RefreshTokenRequest.java +14 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/response/LoginResponse.java +31 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/dto/response/TokenInfoResponse.java +25 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/enums/LoginTypeEnum.java +23 -0
- package/templates/avatar-scaffold-api/src/main/java/com/iflytek/avatar/login/exception/LoginException.java +23 -0
- package/templates/avatar-scaffold-service/k8s-app/Dockerfile +14 -0
- package/templates/avatar-scaffold-service/k8s-app/Dockerfile-arm64 +14 -0
- package/templates/avatar-scaffold-service/packaging/assembly.xml +16 -0
- package/templates/avatar-scaffold-service/pom.xml +150 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/Application.java +21 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/config/LoginConfig.java +20 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/controller/LoginController.java +37 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/converter/LoginConverter.java +54 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/feign/DemoFeign.java +21 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/repository/entity/UserLoginEntity.java +33 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/repository/entity/UserTokenEntity.java +39 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/repository/mapper/UserLoginMapper.java +20 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/service/LoginService.java +22 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/service/impl/LoginServiceImpl.java +43 -0
- package/templates/avatar-scaffold-service/src/main/java/com/iflytek/avatar/login/utils/LoginUtils.java +31 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-dev.yaml +29 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-local.yaml +61 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-prod.yaml +28 -0
- package/templates/avatar-scaffold-service/src/main/resources/application-test.yaml +28 -0
- package/templates/avatar-scaffold-service/src/main/resources/application.yaml +12 -0
- package/templates/pom.xml +98 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integration-test-generator
|
|
3
|
+
description: |
|
|
4
|
+
集成测试生成技能。当用户提到以下关键词时触发:
|
|
5
|
+
integration test, 集成测试, SpringBootTest, MockMvc,
|
|
6
|
+
TestContainers, 端到端测试, E2E test, 接口测试
|
|
7
|
+
|
|
8
|
+
本技能提供 Spring Boot 集成测试的完整模板,包括 @SpringBootTest 配置、
|
|
9
|
+
MockMvc 控制器测试、@MockBean 服务模拟、TestContainers 外部依赖集成、
|
|
10
|
+
测试数据管理和断言模式等。
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# 集成测试生成技能
|
|
14
|
+
|
|
15
|
+
## 适用场景
|
|
16
|
+
|
|
17
|
+
- 为 Controller 层生成集成测试
|
|
18
|
+
- 测试接口的完整请求-响应链路
|
|
19
|
+
- 使用 TestContainers 测试数据库/缓存集成
|
|
20
|
+
- 验证 Spring 上下文的正确装配
|
|
21
|
+
|
|
22
|
+
## @SpringBootTest 基础模板
|
|
23
|
+
|
|
24
|
+
### 最小配置
|
|
25
|
+
|
|
26
|
+
```java
|
|
27
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
28
|
+
@AutoConfigureMockMvc
|
|
29
|
+
@ActiveProfiles("test")
|
|
30
|
+
class UserControllerIntegrationTest {
|
|
31
|
+
|
|
32
|
+
@Autowired
|
|
33
|
+
private MockMvc mockMvc;
|
|
34
|
+
|
|
35
|
+
@Autowired
|
|
36
|
+
private ObjectMapper objectMapper;
|
|
37
|
+
|
|
38
|
+
@Test
|
|
39
|
+
@DisplayName("创建用户集成测试 - 完整链路")
|
|
40
|
+
void should_create_user_successfully() throws Exception {
|
|
41
|
+
// Given
|
|
42
|
+
UserInfoCreateDTO dto = new UserInfoCreateDTO();
|
|
43
|
+
dto.setUsername("integration_test_user");
|
|
44
|
+
dto.setPhone("13812345678");
|
|
45
|
+
dto.setEmail("test@example.com");
|
|
46
|
+
|
|
47
|
+
// When & Then
|
|
48
|
+
mockMvc.perform(post("/api/users")
|
|
49
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
50
|
+
.content(objectMapper.writeValueAsString(dto)))
|
|
51
|
+
.andExpect(status().isOk())
|
|
52
|
+
.andExpect(jsonPath("$.code").value(0))
|
|
53
|
+
.andExpect(jsonPath("$.data").isNumber());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 测试配置文件
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
# src/test/resources/application-test.yml
|
|
62
|
+
spring:
|
|
63
|
+
datasource:
|
|
64
|
+
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL
|
|
65
|
+
driver-class-name: org.h2.Driver
|
|
66
|
+
username: sa
|
|
67
|
+
password:
|
|
68
|
+
sql:
|
|
69
|
+
init:
|
|
70
|
+
mode: always
|
|
71
|
+
schema-locations: classpath:schema-test.sql
|
|
72
|
+
|
|
73
|
+
mybatis-plus:
|
|
74
|
+
mapper-locations: classpath*:/mapper/**/*.xml
|
|
75
|
+
configuration:
|
|
76
|
+
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## MockMvc Controller 测试
|
|
80
|
+
|
|
81
|
+
### GET 请求测试
|
|
82
|
+
|
|
83
|
+
```java
|
|
84
|
+
@Test
|
|
85
|
+
@DisplayName("根据ID查询用户 - 用户存在时返回用户信息")
|
|
86
|
+
void should_return_user_when_id_exists() throws Exception {
|
|
87
|
+
// Given - 预置测试数据
|
|
88
|
+
Long userId = prepareTestUser("testuser", "13800000001");
|
|
89
|
+
|
|
90
|
+
// When & Then
|
|
91
|
+
mockMvc.perform(get("/api/users/{id}", userId)
|
|
92
|
+
.accept(MediaType.APPLICATION_JSON))
|
|
93
|
+
.andExpect(status().isOk())
|
|
94
|
+
.andExpect(jsonPath("$.code").value(0))
|
|
95
|
+
.andExpect(jsonPath("$.data.username").value("testuser"))
|
|
96
|
+
.andExpect(jsonPath("$.data.phone").value("138****0001"))
|
|
97
|
+
.andDo(print()); // 打印请求响应详情,便于调试
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Test
|
|
101
|
+
@DisplayName("根据ID查询用户 - 用户不存在时返回错误")
|
|
102
|
+
void should_return_error_when_id_not_exists() throws Exception {
|
|
103
|
+
mockMvc.perform(get("/api/users/{id}", 999999L)
|
|
104
|
+
.accept(MediaType.APPLICATION_JSON))
|
|
105
|
+
.andExpect(status().isOk())
|
|
106
|
+
.andExpect(jsonPath("$.code").value(not(0)))
|
|
107
|
+
.andExpect(jsonPath("$.message").isNotEmpty());
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### POST 请求测试
|
|
112
|
+
|
|
113
|
+
```java
|
|
114
|
+
@Test
|
|
115
|
+
@DisplayName("创建用户 - 参数校验失败时返回400")
|
|
116
|
+
void should_return_400_when_validation_fails() throws Exception {
|
|
117
|
+
// Given - 缺少必填字段
|
|
118
|
+
UserInfoCreateDTO dto = new UserInfoCreateDTO();
|
|
119
|
+
dto.setUsername(""); // 用户名为空
|
|
120
|
+
|
|
121
|
+
// When & Then
|
|
122
|
+
mockMvc.perform(post("/api/users")
|
|
123
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
124
|
+
.content(objectMapper.writeValueAsString(dto)))
|
|
125
|
+
.andExpect(status().isBadRequest());
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 分页查询测试
|
|
130
|
+
|
|
131
|
+
```java
|
|
132
|
+
@Test
|
|
133
|
+
@DisplayName("分页查询用户列表")
|
|
134
|
+
void should_return_paged_users() throws Exception {
|
|
135
|
+
// Given - 准备多条测试数据
|
|
136
|
+
for (int i = 0; i < 15; i++) {
|
|
137
|
+
prepareTestUser("user_" + i, "1380000" + String.format("%04d", i));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
UserInfoPageQueryDTO queryDTO = new UserInfoPageQueryDTO();
|
|
141
|
+
queryDTO.setPageNum(1);
|
|
142
|
+
queryDTO.setPageSize(10);
|
|
143
|
+
|
|
144
|
+
// When & Then
|
|
145
|
+
mockMvc.perform(post("/api/users/page")
|
|
146
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
147
|
+
.content(objectMapper.writeValueAsString(queryDTO)))
|
|
148
|
+
.andExpect(status().isOk())
|
|
149
|
+
.andExpect(jsonPath("$.code").value(0))
|
|
150
|
+
.andExpect(jsonPath("$.data.records").isArray())
|
|
151
|
+
.andExpect(jsonPath("$.data.records.length()").value(10))
|
|
152
|
+
.andExpect(jsonPath("$.data.total").value(15))
|
|
153
|
+
.andExpect(jsonPath("$.data.pages").value(2));
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### PUT 请求测试
|
|
158
|
+
|
|
159
|
+
```java
|
|
160
|
+
@Test
|
|
161
|
+
@DisplayName("更新用户信息")
|
|
162
|
+
void should_update_user_successfully() throws Exception {
|
|
163
|
+
// Given
|
|
164
|
+
Long userId = prepareTestUser("old_name", "13800000001");
|
|
165
|
+
UserInfoUpdateDTO dto = new UserInfoUpdateDTO();
|
|
166
|
+
dto.setUsername("new_name");
|
|
167
|
+
dto.setEmail("new@example.com");
|
|
168
|
+
|
|
169
|
+
// When & Then
|
|
170
|
+
mockMvc.perform(put("/api/users/{id}", userId)
|
|
171
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
172
|
+
.content(objectMapper.writeValueAsString(dto)))
|
|
173
|
+
.andExpect(status().isOk())
|
|
174
|
+
.andExpect(jsonPath("$.code").value(0));
|
|
175
|
+
|
|
176
|
+
// 验证数据库中已更新
|
|
177
|
+
mockMvc.perform(get("/api/users/{id}", userId))
|
|
178
|
+
.andExpect(jsonPath("$.data.username").value("new_name"))
|
|
179
|
+
.andExpect(jsonPath("$.data.email").value("new@example.com"));
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### DELETE 请求测试
|
|
184
|
+
|
|
185
|
+
```java
|
|
186
|
+
@Test
|
|
187
|
+
@DisplayName("删除用户")
|
|
188
|
+
void should_delete_user_successfully() throws Exception {
|
|
189
|
+
// Given
|
|
190
|
+
Long userId = prepareTestUser("to_delete", "13800000001");
|
|
191
|
+
|
|
192
|
+
// When
|
|
193
|
+
mockMvc.perform(delete("/api/users/{id}", userId))
|
|
194
|
+
.andExpect(status().isOk())
|
|
195
|
+
.andExpect(jsonPath("$.code").value(0));
|
|
196
|
+
|
|
197
|
+
// Then - 验证已删除
|
|
198
|
+
mockMvc.perform(get("/api/users/{id}", userId))
|
|
199
|
+
.andExpect(jsonPath("$.code").value(not(0)));
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## @MockBean 服务模拟
|
|
204
|
+
|
|
205
|
+
当需要隔离外部依赖(如第三方服务调用)时使用 `@MockBean`:
|
|
206
|
+
|
|
207
|
+
```java
|
|
208
|
+
@SpringBootTest
|
|
209
|
+
@AutoConfigureMockMvc
|
|
210
|
+
@ActiveProfiles("test")
|
|
211
|
+
class OrderControllerIntegrationTest {
|
|
212
|
+
|
|
213
|
+
@Autowired
|
|
214
|
+
private MockMvc mockMvc;
|
|
215
|
+
|
|
216
|
+
@Autowired
|
|
217
|
+
private ObjectMapper objectMapper;
|
|
218
|
+
|
|
219
|
+
@MockBean
|
|
220
|
+
private PaymentFeignClient paymentFeignClient;
|
|
221
|
+
|
|
222
|
+
@MockBean
|
|
223
|
+
private SmsService smsService;
|
|
224
|
+
|
|
225
|
+
@Test
|
|
226
|
+
@DisplayName("创建订单 - 模拟支付服务成功")
|
|
227
|
+
void should_create_order_when_payment_succeeds() throws Exception {
|
|
228
|
+
// Given - 模拟外部服务
|
|
229
|
+
when(paymentFeignClient.createPayment(any()))
|
|
230
|
+
.thenReturn(Result.success(new PaymentVO("PAY202301010001")));
|
|
231
|
+
doNothing().when(smsService).sendOrderNotification(any());
|
|
232
|
+
|
|
233
|
+
OrderCreateDTO dto = new OrderCreateDTO();
|
|
234
|
+
dto.setProductId(1L);
|
|
235
|
+
dto.setQuantity(2);
|
|
236
|
+
|
|
237
|
+
// When & Then
|
|
238
|
+
mockMvc.perform(post("/api/orders")
|
|
239
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
240
|
+
.content(objectMapper.writeValueAsString(dto)))
|
|
241
|
+
.andExpect(status().isOk())
|
|
242
|
+
.andExpect(jsonPath("$.code").value(0))
|
|
243
|
+
.andExpect(jsonPath("$.data.orderId").isNotEmpty());
|
|
244
|
+
|
|
245
|
+
// 验证外部服务被调用
|
|
246
|
+
verify(paymentFeignClient, times(1)).createPayment(any());
|
|
247
|
+
verify(smsService, times(1)).sendOrderNotification(any());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Test
|
|
251
|
+
@DisplayName("创建订单 - 支付服务超时时应回滚")
|
|
252
|
+
void should_rollback_when_payment_timeout() throws Exception {
|
|
253
|
+
// Given
|
|
254
|
+
when(paymentFeignClient.createPayment(any()))
|
|
255
|
+
.thenThrow(new FeignException.GatewayTimeout("支付服务超时", null, null, null));
|
|
256
|
+
|
|
257
|
+
OrderCreateDTO dto = new OrderCreateDTO();
|
|
258
|
+
dto.setProductId(1L);
|
|
259
|
+
dto.setQuantity(2);
|
|
260
|
+
|
|
261
|
+
// When & Then
|
|
262
|
+
mockMvc.perform(post("/api/orders")
|
|
263
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
264
|
+
.content(objectMapper.writeValueAsString(dto)))
|
|
265
|
+
.andExpect(status().isOk())
|
|
266
|
+
.andExpect(jsonPath("$.code").value(not(0)))
|
|
267
|
+
.andExpect(jsonPath("$.message").value(containsString("支付")));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## TestContainers 集成
|
|
273
|
+
|
|
274
|
+
### 依赖配置
|
|
275
|
+
|
|
276
|
+
```xml
|
|
277
|
+
<!-- pom.xml -->
|
|
278
|
+
<dependency>
|
|
279
|
+
<groupId>org.testcontainers</groupId>
|
|
280
|
+
<artifactId>testcontainers</artifactId>
|
|
281
|
+
<scope>test</scope>
|
|
282
|
+
</dependency>
|
|
283
|
+
<dependency>
|
|
284
|
+
<groupId>org.testcontainers</groupId>
|
|
285
|
+
<artifactId>mysql</artifactId>
|
|
286
|
+
<scope>test</scope>
|
|
287
|
+
</dependency>
|
|
288
|
+
<dependency>
|
|
289
|
+
<groupId>org.testcontainers</groupId>
|
|
290
|
+
<artifactId>junit-jupiter</artifactId>
|
|
291
|
+
<scope>test</scope>
|
|
292
|
+
</dependency>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### MySQL TestContainer
|
|
296
|
+
|
|
297
|
+
```java
|
|
298
|
+
@SpringBootTest
|
|
299
|
+
@AutoConfigureMockMvc
|
|
300
|
+
@Testcontainers
|
|
301
|
+
class UserRepositoryIntegrationTest {
|
|
302
|
+
|
|
303
|
+
@Container
|
|
304
|
+
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
|
|
305
|
+
.withDatabaseName("test_db")
|
|
306
|
+
.withUsername("test")
|
|
307
|
+
.withPassword("test")
|
|
308
|
+
.withInitScript("schema-test.sql");
|
|
309
|
+
|
|
310
|
+
@DynamicPropertySource
|
|
311
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
312
|
+
registry.add("spring.datasource.url", mysql::getJdbcUrl);
|
|
313
|
+
registry.add("spring.datasource.username", mysql::getUsername);
|
|
314
|
+
registry.add("spring.datasource.password", mysql::getPassword);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@Autowired
|
|
318
|
+
private UserInfoMapper userInfoMapper;
|
|
319
|
+
|
|
320
|
+
@Test
|
|
321
|
+
@DisplayName("MyBatis Plus CRUD 集成测试")
|
|
322
|
+
void should_crud_user_with_real_mysql() {
|
|
323
|
+
// Given
|
|
324
|
+
UserInfo user = new UserInfo();
|
|
325
|
+
user.setUsername("container_test");
|
|
326
|
+
user.setPhone("13812345678");
|
|
327
|
+
|
|
328
|
+
// When - 插入
|
|
329
|
+
int inserted = userInfoMapper.insert(user);
|
|
330
|
+
|
|
331
|
+
// Then
|
|
332
|
+
assertThat(inserted).isEqualTo(1);
|
|
333
|
+
assertThat(user.getId()).isNotNull();
|
|
334
|
+
|
|
335
|
+
// When - 查询
|
|
336
|
+
UserInfo found = userInfoMapper.selectById(user.getId());
|
|
337
|
+
assertThat(found).isNotNull();
|
|
338
|
+
assertThat(found.getUsername()).isEqualTo("container_test");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Redis TestContainer
|
|
344
|
+
|
|
345
|
+
```java
|
|
346
|
+
@Container
|
|
347
|
+
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
|
|
348
|
+
.withExposedPorts(6379);
|
|
349
|
+
|
|
350
|
+
@DynamicPropertySource
|
|
351
|
+
static void configureRedis(DynamicPropertyRegistry registry) {
|
|
352
|
+
registry.add("spring.data.redis.host", redis::getHost);
|
|
353
|
+
registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## 测试数据管理
|
|
358
|
+
|
|
359
|
+
### 数据准备与清理
|
|
360
|
+
|
|
361
|
+
```java
|
|
362
|
+
@SpringBootTest
|
|
363
|
+
@AutoConfigureMockMvc
|
|
364
|
+
@ActiveProfiles("test")
|
|
365
|
+
@Transactional // 每个测试方法执行后自动回滚
|
|
366
|
+
class UserControllerIntegrationTest {
|
|
367
|
+
|
|
368
|
+
@Autowired
|
|
369
|
+
private UserInfoMapper userInfoMapper;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 准备测试用户数据
|
|
373
|
+
*/
|
|
374
|
+
private Long prepareTestUser(String username, String phone) {
|
|
375
|
+
UserInfo user = new UserInfo();
|
|
376
|
+
user.setUsername(username);
|
|
377
|
+
user.setPhone(phone);
|
|
378
|
+
user.setStatus(1);
|
|
379
|
+
user.setCreateTime(LocalDateTime.now());
|
|
380
|
+
userInfoMapper.insert(user);
|
|
381
|
+
return user.getId();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 批量准备测试数据
|
|
386
|
+
*/
|
|
387
|
+
private List<Long> prepareTestUsers(int count) {
|
|
388
|
+
List<Long> ids = new ArrayList<>();
|
|
389
|
+
for (int i = 0; i < count; i++) {
|
|
390
|
+
ids.add(prepareTestUser("user_" + i, "1380000" + String.format("%04d", i)));
|
|
391
|
+
}
|
|
392
|
+
return ids;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### SQL 脚本初始化
|
|
398
|
+
|
|
399
|
+
```sql
|
|
400
|
+
-- src/test/resources/schema-test.sql
|
|
401
|
+
CREATE TABLE IF NOT EXISTS user_info (
|
|
402
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
403
|
+
username VARCHAR(50) NOT NULL,
|
|
404
|
+
phone VARCHAR(20),
|
|
405
|
+
email VARCHAR(100),
|
|
406
|
+
status TINYINT DEFAULT 1,
|
|
407
|
+
deleted TINYINT DEFAULT 0,
|
|
408
|
+
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
409
|
+
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
410
|
+
);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## AssertJ 断言模式
|
|
414
|
+
|
|
415
|
+
```java
|
|
416
|
+
// 基础断言
|
|
417
|
+
assertThat(result).isNotNull();
|
|
418
|
+
assertThat(result.getCode()).isEqualTo(0);
|
|
419
|
+
assertThat(result.getData()).isInstanceOf(UserInfoVO.class);
|
|
420
|
+
|
|
421
|
+
// 集合断言
|
|
422
|
+
assertThat(userList)
|
|
423
|
+
.hasSize(3)
|
|
424
|
+
.extracting(UserInfoVO::getUsername)
|
|
425
|
+
.containsExactlyInAnyOrder("user1", "user2", "user3");
|
|
426
|
+
|
|
427
|
+
// 异常断言
|
|
428
|
+
assertThatThrownBy(() -> userService.getById(null))
|
|
429
|
+
.isInstanceOf(IllegalArgumentException.class)
|
|
430
|
+
.hasMessageContaining("ID不能为空");
|
|
431
|
+
|
|
432
|
+
// 时间断言
|
|
433
|
+
assertThat(user.getCreateTime())
|
|
434
|
+
.isNotNull()
|
|
435
|
+
.isBefore(LocalDateTime.now());
|
|
436
|
+
|
|
437
|
+
// JSON Path 断言(MockMvc)
|
|
438
|
+
mockMvc.perform(get("/api/users/{id}", userId))
|
|
439
|
+
.andExpect(jsonPath("$.code").value(0))
|
|
440
|
+
.andExpect(jsonPath("$.data.username").value("testuser"))
|
|
441
|
+
.andExpect(jsonPath("$.data.phone").value(matchesPattern("\\d{3}\\*{4}\\d{4}")))
|
|
442
|
+
.andExpect(jsonPath("$.data.roles[*].name").value(hasItem("ADMIN")));
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## 最佳实践
|
|
446
|
+
|
|
447
|
+
1. **使用 @Transactional** — 测试方法加 `@Transactional` 自动回滚,保持数据隔离
|
|
448
|
+
2. **随机端口** — 使用 `RANDOM_PORT` 避免端口冲突
|
|
449
|
+
3. **独立 Profile** — 使用 `@ActiveProfiles("test")` 隔离测试配置
|
|
450
|
+
4. **MockBean 最小化** — 只 mock 真正需要隔离的外部依赖
|
|
451
|
+
5. **数据准备方法** — 封装 `prepareXxx()` 方法统一管理测试数据
|
|
452
|
+
6. **断言充分** — 不仅验证响应,还要验证数据库状态变化
|
|
453
|
+
7. **打印调试** — 开发时用 `.andDo(print())`,提交前移除
|
|
454
|
+
8. **测试分层** — 单元测试覆盖逻辑,集成测试覆盖链路
|
|
455
|
+
|
|
456
|
+
## 常见问题排查
|
|
457
|
+
|
|
458
|
+
### 测试上下文启动失败
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
原因: Bean 创建失败或配置缺失
|
|
462
|
+
解决:
|
|
463
|
+
1. 检查 application-test.yml 是否完整
|
|
464
|
+
2. 检查 @MockBean 是否覆盖了必要的外部依赖
|
|
465
|
+
3. 使用 @SpringBootTest(classes = XxxApplication.class) 指定启动类
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### MockMvc 返回 404
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
原因: Controller 未被扫描到
|
|
472
|
+
解决:
|
|
473
|
+
1. 确认测试类和启动类在同一包路径下
|
|
474
|
+
2. 使用 @SpringBootTest 而非 @WebMvcTest(后者只加载 Web 层)
|
|
475
|
+
3. 检查 URL 路径是否正确(注意 context-path)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 数据库相关测试失败
|
|
479
|
+
|
|
480
|
+
```
|
|
481
|
+
原因: H2 和 MySQL 语法差异
|
|
482
|
+
解决:
|
|
483
|
+
1. H2 使用 MODE=MySQL 兼容模式
|
|
484
|
+
2. 使用 TestContainers + 真实 MySQL 获得更好的兼容性
|
|
485
|
+
3. 检查 SQL 中是否有 H2 不支持的语法(如某些函数)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### @Transactional 回滚不生效
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
原因: 异步操作或多线程环境
|
|
492
|
+
解决:
|
|
493
|
+
1. 异步操作的数据变更不在事务管理范围内
|
|
494
|
+
2. 使用 @AfterEach 手动清理数据
|
|
495
|
+
3. 测试中避免使用异步方法
|
|
496
|
+
```
|