package cn.iocoder.yudao.module.mes.service.qc.indicator; import cn.iocoder.yudao.framework.common.exception.ErrorCode; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.mes.controller.admin.qc.indicator.vo.MesQcIndicatorSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO; import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicator.MesQcIndicatorMapper; import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.springframework.context.annotation.Import; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; /** * {@link MesQcIndicatorServiceImpl} 的单元测试 *
* 覆盖分组层级:层级校验、排序号补齐、结果值字段清空、级联删除。 * * @author 超级管理员 */ @Import(MesQcIndicatorServiceImpl.class) public class MesQcIndicatorServiceImplTest extends BaseDbUnitTest { /** * 检测项类型(字典 mes_indicator_type 的「性能」,现网分组行用的就是这个值) */ private static final Integer TYPE_PERFORMANCE = 4; @Resource private MesQcIndicatorServiceImpl indicatorService; @Resource private MesQcIndicatorMapper indicatorMapper; // ==================== 创建:分组项 ==================== @Test public void testCreateGroup_clearsResultFields() { // 准备参数:分组项即使带了 FILE 类型与空的结果值属性,也不应被「结果值属性不能为空」拦住 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T01-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), null, MesQcResultValueTypeEnum.FILE.getType()); reqVO.setFormulaText("w = (m1 - m0) / m × 100%"); // 调用 Long id = indicatorService.createIndicator(reqVO); // 断言:分组项落库时结果值字段被清空,父级归 0,排序号自动补 10(首个顶级) MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(id); assertNotNull(dbIndicator); assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), dbIndicator.getItemType()); assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, dbIndicator.getParentId()); assertNull(dbIndicator.getResultType()); assertNull(dbIndicator.getResultSpecification()); assertEquals("w = (m1 - m0) / m × 100%", dbIndicator.getFormulaText()); assertEquals(10, dbIndicator.getSortOrder()); } @Test public void testCreateGroup_cannotNest() { // mock 数据:一个已存在的分组 MesQcIndicatorDO parent = insertIndicator("T02-G1", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 10); // 准备参数:分组项挂到另一个分组下 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T02-G2", "灰分", MesQcIndicatorItemTypeEnum.GROUP.getType(), parent.getId(), null); // 调用,并断言异常 assertMessageException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_GROUP_CANNOT_NEST, "灰分", "必须作为顶级指标", "水分"); // 断言:未入库 assertEquals(1, indicatorMapper.selectCount()); } // ==================== 创建:录入项 ==================== @Test public void testCreateEntry_defaultSortOrder() { // mock 数据:一个顶级分组,排序号 30 MesQcIndicatorDO group = insertIndicator("T03-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 30); // 调用:组内第一条子项,排序号留空 Long firstId = indicatorService.createIndicator(buildReqVO("T03-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType())); // 调用:组内第二条子项,排序号留空 Long secondId = indicatorService.createIndicator(buildReqVO("T03-E2", "结合水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType())); // 断言:子项步长为 1,依次排到同级末尾 assertEquals(1, indicatorMapper.selectById(firstId).getSortOrder()); assertEquals(2, indicatorMapper.selectById(secondId).getSortOrder()); assertEquals(group.getId(), indicatorMapper.selectById(firstId).getParentId()); } @Test public void testCreateIndicator_defaultSortOrder_topLevelBlockStep() { // mock 数据:顶级分组排序号 30 insertIndicator("T04-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 30); // 调用:顶级新指标排序号留空,顶级步长为 10 Long id = indicatorService.createIndicator(buildReqVO("T04-E", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.TEXT.getType())); // 断言 MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(id); assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, dbIndicator.getParentId()); assertEquals(40, dbIndicator.getSortOrder()); } @Test public void testCreateEntry_parentNotGroup() { // mock 数据:一个录入项被拿来当父级 MesQcIndicatorDO entry = insertIndicator("T05-E1", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 10); // 准备参数 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T05-E2", "色泽", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(), MesQcResultValueTypeEnum.TEXT.getType()); // 调用,并断言异常 assertMessageException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_PARENT_NOT_GROUP, "外观", "不是分组项", "录入项"); } @Test public void testCreateEntry_parentNotExists() { // 准备参数:父级编号不存在 Long notExistsParentId = randomLongId(); MesQcIndicatorSaveReqVO reqVO = buildReqVO("T06-E", "色泽", MesQcIndicatorItemTypeEnum.ENTRY.getType(), notExistsParentId, MesQcResultValueTypeEnum.TEXT.getType()); // 调用,并断言异常 assertMessageException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_PARENT_NOT_EXISTS, "所属分组不存在", String.valueOf(notExistsParentId)); } @Test public void testCreateEntry_resultTypeRequired() { // 准备参数:录入项没有结果值类型 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T07-E", "色泽", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, null); // 调用,并断言异常 assertServiceException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_RESULT_TYPE_REQUIRED); // 断言:未入库 assertEquals(0, indicatorMapper.selectCount()); } @Test public void testCreateEntry_fileResultSpecificationRequired() { // 准备参数:录入项为 FILE 类型但没填结果值属性 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T08-E", "外观照片", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.FILE.getType()); // 调用,并断言异常 assertServiceException(() -> indicatorService.createIndicator(reqVO), QC_INDICATOR_RESULT_SPECIFICATION_REQUIRED); } // ==================== 修改 ==================== @Test public void testUpdateEntry_parentSelf() { // mock 数据:一个录入项 MesQcIndicatorDO entry = insertIndicator("T09-E", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 10); // 准备参数:把自己当父级 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T09-E", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(), MesQcResultValueTypeEnum.TEXT.getType()); reqVO.setId(entry.getId()); // 调用,并断言异常 assertServiceException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_PARENT_SELF); } @Test public void testUpdateIndicator_toGroup_clearsResultFields() { // mock 数据:一个 FILE 类型的录入项 MesQcIndicatorDO entry = insertIndicator("T10-E", "酸含量", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.FILE.getType(), 10); entry.setResultSpecification("IMG"); indicatorMapper.updateById(entry); // 准备参数:把它改成分组项 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T10-E", "酸含量", MesQcIndicatorItemTypeEnum.GROUP.getType(), null, MesQcResultValueTypeEnum.FILE.getType()); reqVO.setId(entry.getId()); reqVO.setFormulaText("c = V × N / m"); // 调用 indicatorService.updateIndicator(reqVO); // 断言:条目类型已改,结果值字段被清空,公式写入 MesQcIndicatorDO dbIndicator = indicatorMapper.selectById(entry.getId()); assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), dbIndicator.getItemType()); assertNull(dbIndicator.getResultType()); assertNull(dbIndicator.getResultSpecification()); assertEquals("c = V × N / m", dbIndicator.getFormulaText()); } @Test public void testUpdateIndicator_hasChildrenCannotChangeToEntry() { // mock 数据:一个带子项的分组 MesQcIndicatorDO group = insertIndicator("T11-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 10); insertIndicator("T11-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1); // 准备参数:把分组改成录入项 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T11-G", "水分", MesQcIndicatorItemTypeEnum.ENTRY.getType(), null, MesQcResultValueTypeEnum.FLOAT.getType()); reqVO.setId(group.getId()); // 调用,并断言异常 assertMessageException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_HAS_CHILDREN, "不能再把它的条目类型改为录入项", "水分"); // 断言:条目类型未被改动 assertEquals(MesQcIndicatorItemTypeEnum.GROUP.getType(), indicatorMapper.selectById(group.getId()).getItemType()); } @Test public void testUpdateIndicator_hasChildrenCannotReparent() { // mock 数据:一个带子项的条目(历史脏数据:录入项也可能被挂过子项,报告侧只认 parent_id) MesQcIndicatorDO entry = insertIndicator("T12-E", "杂质", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.FLOAT.getType(), 10); insertIndicator("T12-E1", "不溶物", MesQcIndicatorItemTypeEnum.ENTRY.getType(), entry.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1); MesQcIndicatorDO group = insertIndicator("T12-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 20); // 准备参数:把带子项的指标挂到分组下 MesQcIndicatorSaveReqVO reqVO = buildReqVO("T12-E", "杂质", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType()); reqVO.setId(entry.getId()); // 调用,并断言异常 assertMessageException(() -> indicatorService.updateIndicator(reqVO), QC_INDICATOR_HAS_CHILDREN, "不能再挂到其它分组下", "杂质"); // 断言:父级未被改动 assertEquals(MesQcIndicatorDO.PARENT_ID_ROOT, indicatorMapper.selectById(entry.getId()).getParentId()); } // ==================== 删除 ==================== @Test public void testDeleteGroup_cascadeChildren() { // mock 数据:一个分组 + 两个子项 + 一个不相关的顶级指标 MesQcIndicatorDO group = insertIndicator("T13-G", "水分", MesQcIndicatorItemTypeEnum.GROUP.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, null, 10); Long child1Id = insertIndicator("T13-E1", "游离水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 1).getId(); Long child2Id = insertIndicator("T13-E2", "结合水", MesQcIndicatorItemTypeEnum.ENTRY.getType(), group.getId(), MesQcResultValueTypeEnum.FLOAT.getType(), 2).getId(); Long otherId = insertIndicator("T13-E3", "外观", MesQcIndicatorItemTypeEnum.ENTRY.getType(), MesQcIndicatorDO.PARENT_ID_ROOT, MesQcResultValueTypeEnum.TEXT.getType(), 20).getId(); // 调用 indicatorService.deleteIndicator(group.getId()); // 断言:分组与其子项一并删除,不相关的指标保留 assertNull(indicatorMapper.selectById(group.getId())); assertNull(indicatorMapper.selectById(child1Id)); assertNull(indicatorMapper.selectById(child2Id)); assertNotNull(indicatorMapper.selectById(otherId)); } @Test public void testDeleteIndicator_notExists() { // 调用,并断言异常 assertServiceException(() -> indicatorService.deleteIndicator(randomLongId()), QC_INDICATOR_NOT_EXISTS); } // ==================== 辅助方法 ==================== private MesQcIndicatorSaveReqVO buildReqVO(String code, String name, Integer itemType, Long parentId, Integer resultType) { MesQcIndicatorSaveReqVO reqVO = new MesQcIndicatorSaveReqVO(); reqVO.setCode(code); reqVO.setName(name); reqVO.setType(TYPE_PERFORMANCE); reqVO.setItemType(itemType); reqVO.setParentId(parentId); reqVO.setResultType(resultType); return reqVO; } private MesQcIndicatorDO insertIndicator(String code, String name, Integer itemType, Long parentId, Integer resultType, Integer sortOrder) { MesQcIndicatorDO indicator = new MesQcIndicatorDO(); indicator.setCode(code); indicator.setName(name); indicator.setType(TYPE_PERFORMANCE); indicator.setItemType(itemType); indicator.setParentId(parentId); indicator.setResultType(resultType); indicator.setSortOrder(sortOrder); indicatorMapper.insert(indicator); return indicator; } /** * 断言抛出的业务异常错误码正确,且提示文案包含给定的关键片段。 *
* 层级校验的文案带分组名 / 父指标名 / 子项条数,无法用 * {@link cn.iocoder.yudao.framework.test.core.util.AssertUtils#assertServiceException} 逐字比对。 */ private void assertMessageException(Executable executable, ErrorCode errorCode, String... messageFragments) { ServiceException exception = assertThrows(ServiceException.class, executable); assertEquals(errorCode.getCode(), exception.getCode(), "错误码不匹配"); for (String fragment : messageFragments) { assertTrue(exception.getMessage().contains(fragment), String.format("错误提示[%s]未包含[%s]", exception.getMessage(), fragment)); } } }