Files
openclaw-config/workspace-planner/docs/单元测试规范.md
T

545 lines
13 KiB
Markdown

# mica 项目单元测试规范
## 1. 测试框架与依赖
### 1.1 核心依赖
```xml
<!-- Spring Boot Test (包含 JUnit 5, Mockito, AssertJ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito Inline (支持静态方法 mock) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
```
### 1.2 测试目录结构
```
src/test/
├── java/
│ └── com/witsoft/mica/
│ ├── smd/
│ │ ├── controller/
│ │ │ └── ItemControllerTest.java
│ │ ├── service/
│ │ │ └── ItemServiceTest.java
│ │ └── mapper/
│ │ └── ItemMapperTest.java
│ └── ...
└── resources/
└── application-test.yml
```
---
## 2. Controller 层测试规范
### 2.1 测试类结构
```java
package com.witsoft.mica.smd.controller;
import com.witsoft.gen.base.common.ResponseModel;
import com.witsoft.gen.base.page.PageDomain;
import com.witsoft.mica.smd.dto.ItemQueryDTO;
import com.witsoft.mica.smd.service.ItemService;
import com.witsoft.mica.smd.vo.ItemVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("物料信息管理 Controller 测试")
class ItemControllerTest {
@Mock
private ItemService itemService;
@InjectMocks
private ItemController itemController;
private ItemQueryDTO queryDTO;
private PageDomain<ItemVO> pageDomain;
@BeforeEach
void setUp() {
// 准备测试数据
queryDTO = new ItemQueryDTO();
queryDTO.setPageNo(1);
queryDTO.setPageSize(10);
queryDTO.setEcid("test-ecid-001");
pageDomain = new PageDomain<>(1, 10, 1);
List<ItemVO> list = new ArrayList<>();
ItemVO itemVO = new ItemVO();
itemVO.setId("1");
itemVO.setItemCode("ITEM001");
list.add(itemVO);
pageDomain.setList(list);
}
@Test
@DisplayName("分页查询 - 成功")
void testQueryPageListSuccess() {
// Arrange
when(itemService.queryPageList(any(ItemQueryDTO.class))).thenReturn(pageDomain);
// Act
ResponseModel<PageDomain<ItemVO>> response = itemController.queryPageList(queryDTO);
// Assert
assertNotNull(response);
assertTrue(response.isSuccess());
assertNotNull(response.getData());
assertEquals(1, response.getData().getTotal());
verify(itemService, times(1)).queryPageList(any(ItemQueryDTO.class));
}
}
```
### 2.2 测试要点
#### ✅ 必须测试的场景
1. **成功场景** - 正常业务流程
2. **参数校验** - 空值、非法值处理
3. **异常处理** - Service 层异常捕获
4. **边界条件** - 空列表、null 值
#### ⚠️ 项目特殊挑战
由于项目使用了 `GlobalUtils.getEcid()``ResponseModel.succeed()` 等依赖 Spring 上下文的静态方法,纯单元测试会遇到困难。
**解决方案:**
**方案 A:集成测试(推荐)**
```java
@SpringBootTest
@AutoConfigureMockMvc
class ItemControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void testQueryPageList() throws Exception {
mockMvc.perform(post("/web/itemInfo/getPageList")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"pageNo\":1,\"pageSize\":10}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
}
}
```
**方案 B:Service 层单元测试(优先)**
- Controller 层逻辑简单,主要通过集成测试覆盖
- Service 层使用 Mockito 进行纯单元测试
---
## 3. Service 层测试规范
### 3.1 测试类结构
```java
package com.witsoft.mica.smd.service;
import com.witsoft.gen.base.page.PageDomain;
import com.witsoft.mica.smd.dto.ItemQueryDTO;
import com.witsoft.mica.smd.mapper.ItemMapper;
import com.witsoft.mica.smd.vo.ItemVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("物料信息服务测试")
class ItemServiceTest {
@Mock
private ItemMapper itemMapper;
@InjectMocks
private ItemServiceImpl itemService;
private ItemQueryDTO queryDTO;
private List<ItemVO> mockList;
@BeforeEach
void setUp() {
queryDTO = new ItemQueryDTO();
queryDTO.setPageNo(1);
queryDTO.setPageSize(10);
mockList = new ArrayList<>();
ItemVO item = new ItemVO();
item.setId("1");
item.setItemCode("ITEM001");
mockList.add(item);
}
@Test
@DisplayName("分页查询 - 成功")
void testQueryPageListSuccess() {
// Arrange
when(itemMapper.queryPageCount(any(ItemQueryDTO.class))).thenReturn(1L);
when(itemMapper.queryPageList(any(ItemQueryDTO.class))).thenReturn(mockList);
// Act
PageDomain<ItemVO> result = itemService.queryPageList(queryDTO);
// Assert
assertNotNull(result);
assertEquals(1, result.getTotal());
assertEquals(1, result.getList().size());
assertEquals("ITEM001", result.getList().get(0).getItemCode());
verify(itemMapper, times(1)).queryPageCount(any(ItemQueryDTO.class));
verify(itemMapper, times(1)).queryPageList(any(ItemQueryDTO.class));
}
@Test
@DisplayName("分页查询 - 无数据")
void testQueryPageListEmpty() {
// Arrange
when(itemMapper.queryPageCount(any(ItemQueryDTO.class))).thenReturn(0L);
when(itemMapper.queryPageList(any(ItemQueryDTO.class))).thenReturn(new ArrayList<>());
// Act
PageDomain<ItemVO> result = itemService.queryPageList(queryDTO);
// Assert
assertNotNull(result);
assertEquals(0, result.getTotal());
assertTrue(result.getList().isEmpty());
}
@Test
@DisplayName("详情查询 - 成功")
void testQueryByIdSuccess() {
// Arrange
String testId = "123";
ItemVO mockItem = new ItemVO();
mockItem.setId(testId);
mockItem.setItemCode("ITEM001");
when(itemMapper.selectById(testId)).thenReturn(mockItem);
// Act
ItemVO result = itemService.queryById(testId);
// Assert
assertNotNull(result);
assertEquals(testId, result.getId());
assertEquals("ITEM001", result.getItemCode());
}
@Test
@DisplayName("详情查询 - 不存在")
void testQueryByIdNotFound() {
// Arrange
when(itemMapper.selectById("not-exist")).thenReturn(null);
// Act
ItemVO result = itemService.queryById("not-exist");
// Assert
assertNull(result);
}
}
```
### 3.2 Feign 调用测试
```java
@Test
@DisplayName("新增物料 - Feign 调用成功")
void testInsertItemSuccess() {
// Arrange
ItemFormDTO dto = new ItemFormDTO();
dto.setItemCode("ITEM001");
dto.setItemName("测试物料");
ResponseModel mockResponse = ResponseModel.succeed(null);
when(itemApi.insertItem(any(Map.class))).thenReturn(mockResponse);
// Act
ResponseModel result = itemService.insertItem(dto);
// Assert
assertNotNull(result);
assertTrue(result.isSuccess());
verify(itemApi, times(1)).insertItem(any(Map.class));
}
```
---
## 4. Mapper 层测试规范
### 4.1 使用 MyBatis Spring Boot Test
```xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
```
### 4.2 测试类结构
```java
package com.witsoft.mica.smd.mapper;
import com.witsoft.mica.smd.dto.ItemQueryDTO;
import com.witsoft.mica.smd.vo.ItemVO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ItemMapperTest {
@Autowired
private ItemMapper itemMapper;
@Test
void testQueryPageList() {
// Arrange
ItemQueryDTO dto = new ItemQueryDTO();
dto.setPageNo(1);
dto.setPageSize(10);
dto.setEcid("test-ecid");
// Act
List<ItemVO> list = itemMapper.queryPageList(dto);
long total = itemMapper.queryPageCount(dto);
// Assert
assertNotNull(list);
assertTrue(total >= 0);
}
@Test
void testSelectById() {
// Act
ItemVO item = itemMapper.selectById("1");
// Assert
assertNotNull(item);
assertNotNull(item.getItemCode());
}
}
```
---
## 5. 测试命名规范
### 5.1 测试类命名
```
{被测试类名}Test.java
示例:ItemControllerTest.java, ItemServiceTest.java
```
### 5.2 测试方法命名
```
test{方法名}{场景}.java
示例:
- testQueryPageListSuccess()
- testQueryPageListWithEmptyParams()
- testQueryPageListServiceException()
- testInsertItemValidationFailed()
```
### 5.3 DisplayName 注解
```java
@DisplayName("分页查询 - 成功")
@DisplayName("分页查询 - 参数为空时返回空结果")
@DisplayName("分页查询 - 服务异常处理")
@DisplayName("新增物料 - 参数校验失败")
```
---
## 6. 断言规范
### 6.1 常用断言
```java
// 非空断言
assertNotNull(result);
assertNull(result);
// 布尔值断言
assertTrue(response.isSuccess());
assertFalse(response.isSuccess());
// 相等断言
assertEquals(1, result.getTotal());
assertEquals("ITEM001", result.getItemCode());
// 集合断言
assertTrue(list.isEmpty());
assertEquals(1, list.size());
// 异常断言
assertThrows(Exception.class, () -> {
itemController.deleteItem("");
});
```
### 6.2 Mockito 验证
```java
// 验证方法调用次数
verify(itemService, times(1)).queryPageList(any());
verify(itemService, never()).deleteItem(any());
// 验证调用参数
verify(itemService).insertItem(argThat(dto ->
dto.getItemCode() != null &&
dto.getEcid() != null
));
```
---
## 7. 测试覆盖率要求
| 层级 | 覆盖率要求 | 优先级 |
|------|-----------|--------|
| Service | ≥ 80% | 高 |
| Controller | ≥ 70% | 中 |
| Mapper | ≥ 60% | 中 |
| Entity/DTO | 不要求 | 低 |
---
## 8. 测试执行命令
```bash
# 运行所有测试
mvn test
# 运行指定测试类
mvn test -Dtest=ItemControllerTest
# 运行指定测试方法
mvn test -Dtest=ItemControllerTest#testQueryPageListSuccess
# 生成覆盖率报告
mvn clean test jacoco:report
```
---
## 9. 最佳实践
### ✅ DO
1. **测试独立** - 每个测试方法相互独立,不依赖执行顺序
2. **命名清晰** - 测试方法名清晰表达测试意图
3. **Arrange-Act-Assert** - 遵循 AAA 模式组织测试代码
4. **测试边界** - 重点测试边界条件和异常情况
5. **Mock 外部依赖** - 使用 Mockito 隔离外部依赖
### ❌ DON'T
1. **不要测试私有方法** - 通过公共方法间接测试
2. **不要过度测试** - 聚焦业务逻辑,不测试 getter/setter
3. **不要依赖外部状态** - 每个测试自包含,不依赖数据库真实数据
4. **不要忽略失败** - 测试失败必须修复或合理解释
---
## 10. 示例:完整测试类
参考 `ItemControllerTest.java``ItemServiceTest.java`
---
## 11. 持续集成
### Jenkins Pipeline 配置
```groovy
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'mvn clean test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
stage('Coverage') {
steps {
sh 'mvn jacoco:report'
}
post {
always {
jacoco execPattern: 'target/jacoco.exec'
}
}
}
}
}
```
---
## 12. 参考资料
- [JUnit 5 官方文档](https://junit.org/junit5/docs/current/user-guide/)
- [Mockito 官方文档](https://site.mockito.org/)
- [Spring Boot Testing](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)
- [MyBatis Spring Boot Test](https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/)