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} 的单元测试
|
* <p>
|
* 覆盖分组层级:层级校验、排序号补齐、结果值字段清空、级联删除。
|
*
|
* @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;
|
}
|
|
/**
|
* 断言抛出的业务异常错误码正确,且提示文案包含给定的关键片段。
|
* <p>
|
* 层级校验的文案带分组名 / 父指标名 / 子项条数,无法用
|
* {@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));
|
}
|
}
|
|
}
|