545 lines
13 KiB
Markdown
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/)
|