# mica 项目单元测试规范 ## 1. 测试框架与依赖 ### 1.1 核心依赖 ```xml org.springframework.boot spring-boot-starter-test test org.mockito mockito-inline 4.6.1 test ``` ### 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 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 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> 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 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 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 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 org.mybatis.spring.boot mybatis-spring-boot-starter-test 2.3.1 test ``` ### 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 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/)