# Validator 校验器测试完整示例

> 以 `OutSendProcessQtyValidator` 为例，展示校验器的完整测试写法。

```java
package kd.mmc.sfc.opplugin.validator;

import kd.bos.dataentity.entity.DynamicObject;
import kd.bos.entity.ExtendedDataEntity;
import kd.bos.entity.validate.ErrorLevel;
import kd.bos.test.ext.annotaions.UnittestCaseInfo;
import kd.bos.unittest.AbstractJunitNoDependenciesResManagerTest;
import kd.bos.unittest.mock.DynamicObjectMocker;
import kd.mmc.sfc.business.OutSourceSendQtyCheckHelper;
import kd.mmc.sfc.business.OutsourceSendHelper;
import kd.mmc.sfc.business.ProcessPlanHelper;
import kd.mmc.sfc.common.insideaccept.InsideAcceptConst;
import kd.mmc.sfc.common.outsourcesend.OutsourceSendConsts;
import kd.mmc.sfc.opplugin.Validator.OutSendProcessQtyValidator;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import java.math.BigDecimal;
import java.util.*;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

public class OutSendProcessQtyValidatorTest extends AbstractJunitNoDependenciesResManagerTest {

    // ========== 1. Mock 声明 ==========
    private MockedStatic<OutsourceSendHelper> outsourceSendHelper;
    private MockedStatic<ProcessPlanHelper> processPlanHelper;
    private MockedStatic<OutSourceSendQtyCheckHelper> outSourceSendQtyCheckHelper;

    // ========== 2. 错误收集器 ==========
    private List<Integer> errorIndexs = new ArrayList<>();
    private final List<String> errorMessages = new ArrayList<>();

    @Before
    public void before() {
        MockitoAnnotations.openMocks(this);
        outsourceSendHelper = Mockito.mockStatic(OutsourceSendHelper.class);
        processPlanHelper = Mockito.mockStatic(ProcessPlanHelper.class);
        outSourceSendQtyCheckHelper = Mockito.mockStatic(OutSourceSendQtyCheckHelper.class);
    }

    @After
    public void after() {
        outsourceSendHelper.close();
        processPlanHelper.close();
        outSourceSendQtyCheckHelper.close();
    }

    // ========== 3. 构建校验器（核心：匿名子类重写消息方法）==========
    private OutSendProcessQtyValidator buildValidator() {
        return new OutSendProcessQtyValidator() {
            @Override
            protected void addMessage(ExtendedDataEntity dataEntity, String content) {
                errorIndexs.add(dataEntity.getDataEntityIndex());
                errorMessages.add(content);
            }
            @Override
            protected void addMessage(ExtendedDataEntity dataEntity, String content, ErrorLevel errorLevel) {
                errorIndexs.add(dataEntity.getDataEntityIndex());
                errorMessages.add(content);
            }
            @Override
            protected void addErrorMessage(ExtendedDataEntity dataEntity, String content) {
                errorIndexs.add(dataEntity.getDataEntityIndex());
                errorMessages.add(content);
            }
            @Override
            protected void addWarningMessage(ExtendedDataEntity dataEntity, String content) {
                errorIndexs.add(dataEntity.getDataEntityIndex());
                errorMessages.add(content);
            }
        };
    }

    // ========== 4. 构建测试数据（DynamicObjectMocker 复杂嵌套）==========
    private ExtendedDataEntity[] buildValidatorData() {
        // 单据一：数量超出（应触发校验错误）
        DynamicObject billOne = new DynamicObjectMocker()
            .add("id", 123456L)
            .add(OutsourceSendConsts.BILLTYPEID_ID, 12334L)
            .add(OutsourceSendConsts.BILLSTATUS, "A")
            .add("entryentity", new DynamicObjectMocker()
                .add("id", 1L)
                .add("seq", 1)
                .add("proplanentry_id", 2L)
                .add("proplanentry", new DynamicObjectMocker()
                    .add("id", 2L)
                    .add("headunitfactor", 1)
                    .add("prounitfactor", 1)
                    .add("processsequence", 1)
                    .add("processnumber", 10).getObject())
                .add("proplan_id", 3L)
                .add("material", new DynamicObjectMocker()
                    .add("masterid.id", 4L).getObject())
                .add("processqty", new BigDecimal(11))    // 超出 cansendqty
                .add("cansendqty", new BigDecimal(10))
                .add("unit", new DynamicObjectMocker()
                    .add("id", 1L)
                    .add("precision", 1).getObject())
                .getCollection())
            .getObject();

        // 单据二：数量正常（校验通过）
        DynamicObject billTwo = new DynamicObjectMocker()
            .add("id", 123456L)
            .add(OutsourceSendConsts.BILLTYPEID_ID, 12334L)
            .add(OutsourceSendConsts.BILLSTATUS, "A")
            .add("entryentity", new DynamicObjectMocker()
                .add("id", 1L)
                .add("seq", 1)
                .add("proplanentry_id", 2000000L)
                .add("proplanentry", new DynamicObjectMocker()
                    .add("id", 2000000L)
                    .add("headunitfactor", 1)
                    .add("prounitfactor", 1)
                    .add("processsequence", 1)
                    .add("processnumber", 20).getObject())
                .add("proplan_id", 30000000L)
                .add("material", new DynamicObjectMocker()
                    .add("masterid.id", 4L).getObject())
                .add("processqty", BigDecimal.TEN)         // 等于 cansendqty
                .add("cansendqty", BigDecimal.TEN)
                .add("unit", new DynamicObjectMocker()
                    .add("id", 1L)
                    .add("precision", 1).getObject())
                .getCollection())
            .getObject();

        return new ExtendedDataEntity[]{
            new ExtendedDataEntity(billOne, 0, 0),
            new ExtendedDataEntity(billTwo, 0, 0)
        };
    }

    // ========== 5. 测试方法 ==========

    @UnittestCaseInfo(
        author = "zhang_san <zhang_san@kingdee.com>",
        title = "Test validate should not generate error",
        targetClass = "kd.mmc.sfc.opplugin.validator.OutSendProcessQtyValidator",
        targetMethod = "validate",
        lastUpdateTime = "2025-08-05 19:54:35",
        lastUpdateAuthor = "zhang_san <zhang_san@kingdee.com>",
        methodSignature = "null",
        testPoints = {"Functionality"},
        description = "Test validate should not generate error"
    )
    @Test
    public void testValidate_ShouldNotGenerateError() {
        //step 配置 Mock（所有外部查询返回空/默认值）
        processPlanHelper.when(() -> ProcessPlanHelper.getPlanInfo(any(Set.class)))
            .thenReturn(new HashMap<>());
        outsourceSendHelper.when(() -> OutsourceSendHelper.getOutSendInfo(any(Set.class)))
            .thenReturn(new HashMap<>());
        outsourceSendHelper.when(() -> OutsourceSendHelper.getRelatedProcess(any()))
            .thenReturn(new ArrayList<>());

        //step 构建校验器和数据
        ExtendedDataEntity[] dataEntities = buildValidatorData();
        OutSendProcessQtyValidator validator = buildValidator();
        validator.setEntityKey(InsideAcceptConst.FORMID);
        validator.setDataEntities(dataEntities);

        //step 执行校验
        validator.validate();

        //assert 无错误
        assertEquals(0, errorIndexs.size());
    }

    @UnittestCaseInfo(
        author = "zhang_san <zhang_san@kingdee.com>",
        title = "Test validate should generate error",
        targetClass = "kd.mmc.sfc.opplugin.validator.OutSendProcessQtyValidator",
        targetMethod = "validate",
        lastUpdateTime = "2025-08-05 19:54:35",
        lastUpdateAuthor = "zhang_san <zhang_san@kingdee.com>",
        methodSignature = "null",
        testPoints = {"Functionality"},
        description = "Test validate should generate error"
    )
    @Test
    public void testValidate_ShouldGenerateError() {
        //step 配置 Mock（构造会触发校验失败的数据环境）
        Map<Long, Map<String, Object>> planInfos = new HashMap<>();
        Map<String, Object> map = new HashMap<>(16);
        map.put("id", 1L);
        map.put("billno", "GXJH-20231207-00000167");
        map.put("entryentity.higherqty", BigDecimal.ZERO);
        map.put("entryentity.outsourcedsbqty", BigDecimal.ZERO);
        map.put("entryentity.outsourcedqty", BigDecimal.ZERO);
        map.put("entryentity.reportordercontrol", "B");
        planInfos.put(2L, map);

        processPlanHelper.when(() -> ProcessPlanHelper.getPlanInfo(any(Set.class)))
            .thenReturn(planInfos);
        outsourceSendHelper.when(() -> OutsourceSendHelper.getOutSendInfo(any(Set.class)))
            .thenReturn(new HashMap<>());
        outsourceSendHelper.when(() -> OutsourceSendHelper.getRelatedProcess(any()))
            .thenReturn(new ArrayList<>());

        //step 执行校验
        ExtendedDataEntity[] dataEntities = buildValidatorData();
        OutSendProcessQtyValidator validator = buildValidator();
        validator.setEntityKey(InsideAcceptConst.FORMID);
        validator.setDataEntities(dataEntities);
        validator.validate();

        //assert 应有 2 条错误
        assertEquals(2, errorIndexs.size());
    }
}
```

## 要点总结

1. **匿名子类重写** `addMessage` / `addErrorMessage` / `addWarningMessage` 收集错误到 List
2. **ExtendedDataEntity 包装**：`new ExtendedDataEntity(dynamicObject, rowIndex, 0)`
3. **setEntityKey** 设置单据标识（部分校验器依赖此值判断逻辑分支）
4. **setDataEntities** 设置校验数据后再调用 `validate()`
5. **断言错误数量**：`assertEquals(expectedCount, errorMessages.size())`
6. 构造**通过和不通过两组数据**，分别验证正常和异常路径
