一键导入
nbl-java-spring-integration-testing
Use when writing Java Spring Boot integration tests that perform real database CRUD operations with MockMvc and MyBatis-Plus
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when writing Java Spring Boot integration tests that perform real database CRUD operations with MockMvc and MyBatis-Plus
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when executing implementation plans with independent tasks in the current session
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when you have a spec or requirements for a multi-step task, before touching code
Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
Use when creating new skills, editing existing skills, or verifying skills work before deployment
| name | nbl.java-spring-integration-testing |
| description | Use when writing Java Spring Boot integration tests that perform real database CRUD operations with MockMvc and MyBatis-Plus |
背景:管理功能(如部门、用户、角色、配置等)的核心是 CRUD 操作。这些功能的特殊性在于:
- 多表关联:主表 + 多种关联关系表(人员关系、业务关系等)
- 数据持久化:需要验证数据真正写入数据库,而不仅是 HTTP 响应
- 回归风险高:修改后需要确保已有功能不受影响
本 skill 提供一套经过验证的集成测试模式,覆盖以上场景。
使用 BaseIntegrationTest 作为测试基类,执行真实数据库 CRUD 操作,测试结束后自动回滚保证幂等性。
核心特点:
@Transactional 保证测试结束后数据自动回滚Use when:
When NOT to Use:
class FormControllerTest extends BaseIntegrationTest {
@Autowired
private FormMapper formMapper;
BaseIntegrationTest 提供:
@SpringBootTest + @AutoConfigureMockMvc + @TransactionalmockMvc - 已注入,可直接使用objectMapper - 已注入,可直接使用parseData(json, Class) - 解析响应 data 字段为对象parseDataAsLong(json) - 解析响应 data 字段为 LongTEST_TENANT_ID / OPERATOR_ID - 测试用常量项目应提供 BaseIntegrationTest 基类,包含:
protected static final Long TEST_TENANT_ID = 1L;
protected static final Long OPERATOR_ID = 1L;
class FormControllerTest extends BaseIntegrationTest {
@Autowired
private FormMapper formMapper;
@Test
void testCRUD() throws Exception {
String formName = "TestForm-" + System.currentTimeMillis();
String formKey = "FormKey_" + System.currentTimeMillis();
// ========== 1. Create ==========
Long formId = createForm(formName, formKey);
assertNotNull(formId, "创建表单后应返回ID");
// 验证主表字段
Form form = formMapper.selectById(formId);
assertNotNull(form, "表单创建后应存在于数据库");
assertEquals(createdName, form.getFormName());
assertEquals(TEST_TENANT_ID, form.getTenantId());
assertEquals(OPERATOR_ID, form.getCreatedBy());
// ========== 2. GetById ==========
FormDetailResp detailResp = getFormById(formId);
assertNotNull(detailResp);
assertEquals(formId, detailResp.getId());
// ========== 3. Update ==========
String updatedName = "UpdatedForm-" + System.currentTimeMillis();
updateForm(formId, updatedName);
// 验证主表更新
Form updated = formMapper.selectById(formId);
assertEquals(updatedName, updated.getFormName());
assertEquals(OPERATOR_ID, updated.getUpdatedBy());
}
// ==================== 辅助方法 ====================
private Long createForm(String formName, String formKey) throws Exception {
FormInsertReq insertReq = new FormInsertReq();
insertReq.setFormName(formName);
insertReq.setFormKey(formKey);
insertReq.setBusinessTypeCode("LEAVE");
insertReq.setTenantId(TEST_TENANT_ID);
insertReq.setOperatorId(OPERATOR_ID);
MvcResult result = performPost("/forms/insert", insertReq).andReturn();
return parseDataAsLong(result.getResponse().getContentAsString());
}
private FormDetailResp getFormById(Long id) throws Exception {
MvcResult result = mockMvc.perform(get("/forms/get-by-id")
.param("id", id.toString())
.param("tenantId", TEST_TENANT_ID.toString()))
.andExpect(status().isOk())
.andReturn();
return parseData(result.getResponse().getContentAsString(), FormDetailResp.class);
}
private void updateForm(Long id, String updatedName) throws Exception {
FormUpdateReq updateReq = new FormUpdateReq();
updateReq.setId(id);
updateReq.setFormName(updatedName);
updateReq.setBusinessTypeCode("LEAVE");
updateReq.setTenantId(TEST_TENANT_ID);
updateReq.setOperatorId(OPERATOR_ID);
performPost("/forms/update", updateReq);
}
private ResultActions performPost(String url, Object req) throws Exception {
return mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk());
}
}
| 原则 | 说明 |
|---|---|
| 真实写库 | 不 Mock 数据层,真正写入数据库验证 |
| 自动回滚 | @Transactional 保证测试结束数据回滚 |
| DB 验证 | 通过项目的 Mapper 直接查询数据库验证状态 |
| 继承基类 | 继承 BaseIntegrationTest 获取通用能力 |
| 时间戳命名 | System.currentTimeMillis() 避免数据冲突 |
| 幂等性 | 可重复运行多次,结果一致 |
项目应提供类似以下基类:
@SpringBootTest(
classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"app.id=20005",
"apollo.meta=http://172.20.0.138:38080",
"apollo.bootstrap.enabled=true",
"apollo.bootstrap.namespaces=application,GUOGUOTECH.APPLICATION2",
"spring.application.name=xxx",
"spring.cloud.bootstrap.enabled=true"
}
)
@AutoConfigureMockMvc
@Transactional
public abstract class BaseIntegrationTest {
protected static final Long TEST_TENANT_ID = 1L;
protected static final Long OPERATOR_ID = 1L;
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
protected <T> T parseData(String json, Class<T> clazz) throws Exception {
JsonNode root = objectMapper.readTree(json);
return objectMapper.readValue(root.get("data").toString(), clazz);
}
protected Long parseDataAsLong(String json) throws Exception {
JsonNode root = objectMapper.readTree(json);
return root.get("data").asLong();
}
}
is_deleted=1 的记录assertNull(mapper.selectById(id))❌ 错误:不验证数据库,只依赖 HTTP 响应
// 只验证 status 200,不验证数据库状态 -> 不完整
mockMvc.perform(...).andExpect(status().isOk());
✅ 正确:验证 HTTP 状态,再查数据库验证真实状态
mockMvc.perform(...).andExpect(status().isOk());
Form form = formMapper.selectById(formId);
assertNotNull(form);
assertEquals(expectedName, form.getFormName());
❌ 错误:每次测试手动清理数据
// 不需要手动删除,@Transactional 自动回滚
@AfterEach
void cleanup() {
formMapper.deleteById(formId);
}
✅ 正确:依赖 @Transactional 自动回滚
class FormControllerTest extends BaseIntegrationTest { ... }
❌ 错误:使用 @WebMvcTest + Mock Repository
@WebMvcTest(UserController.class) // 只加载 Web 层
class UserControllerTest {
@MockBean
private UserRepository repository; // Mock 不执行真实写库
}
✅ 正确:继承 BaseIntegrationTest 启动完整上下文
@SpringBootTest(...)
@AutoConfigureMockMvc
@Transactional
class UserControllerTest extends BaseIntegrationTest {
@Autowired
private UserRepository repository; // 真实 Repository,真实写库
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.junit.jupiter.api.Assertions.*;