# OpPlugin 操作插件测试完整示例

> 以 `ProcessPlanSaveOp` 为例，展示操作插件的完整测试写法。

```java
package kd.mmc.sfc.opplugin.processplan;

import kd.bos.dataentity.OperateOption;
import kd.bos.dataentity.entity.DynamicObject;
import kd.bos.dataentity.entity.DynamicObjectCollection;
import kd.bos.entity.operate.result.OperationResult;
import kd.bos.entity.plugin.args.BeforeOperationArgs;
import kd.bos.entity.plugin.args.BeginOperationTransactionArgs;
import kd.bos.exception.KDBizException;
import kd.bos.form.unittest.DisplayName;
import kd.bos.org.utils.Consts;
import kd.bos.servicehelper.BusinessDataServiceHelper;
import kd.bos.test.ext.annotaions.UnittestCaseInfo;
import kd.bos.unittest.AbstractJunitNoDependenciesResManagerTest;
import kd.bos.unittest.mock.DynamicObjectMocker;
import kd.mmc.sfc.business.OperationHelper;
import kd.mmc.sfc.business.ProcessPickingHelper;
import kd.mmc.sfc.business.ProcessPlanHelper;
import kd.mmc.sfc.common.processplan.ProcessPlanConsts;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;

import java.util.*;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * 工序计划保存操作单元测试
 * @author zhang_san
 * @date 2025/5/14 10:50
 */
public class ProcessPlanSaveOpTest extends AbstractJunitNoDependenciesResManagerTest {

    // ========== 1. 声明 MockedStatic ==========
    MockedStatic<ProcessPickingHelper> processPickingHelper;
    private BeginOperationTransactionArgs beginOperationTransactionArgs;

    // ========== 2. @Before ==========
    @Before
    public void before() {
        processPickingHelper = mockStatic(ProcessPickingHelper.class);

        // 构建测试数据
        DynamicObject[] planS = getBillS();

        // Mock 事务参数
        beginOperationTransactionArgs = mock(BeginOperationTransactionArgs.class);
        when(beginOperationTransactionArgs.getDataEntities()).thenReturn(planS);
        when(beginOperationTransactionArgs.getOperationKey())
            .thenReturn(ProcessPlanConsts.OPERATION_CONST.SAVE);
    }

    // ========== 3. @After ==========
    @After
    public void after() {
        processPickingHelper.close();
    }

    public void reset() {
        processPickingHelper.reset();
    }

    // ========== 4. 构建测试单据 ==========
    private DynamicObject[] getBillS() {
        Set<DynamicObject> billSet = new HashSet<>();
        billSet.add(new DynamicObjectMocker()
            .add(Consts.ID, 2128998950412872704L)
            .add(ProcessPlanConsts.BILLNO, "GXJH-20231109-00000047")
            .add(ProcessPlanConsts.ROOTPROCESSPLAN_ID, 0L)
            .add(ProcessPlanConsts.WORKROWID, 11L)
            .getObject());
        billSet.add(new DynamicObjectMocker()
            .add(Consts.ID, 1814710693405206528L)
            .add(ProcessPlanConsts.BILLNO, "GXJH-20231109-00000048")
            .add(ProcessPlanConsts.ROOTPROCESSPLAN_ID, 996L)
            .add(ProcessPlanConsts.WORKROWID, 22L)
            .getObject());
        return billSet.toArray(new DynamicObject[0]);
    }

    // ========== 5. 测试方法 ==========

    @UnittestCaseInfo(
        author = "zhang_san <zhang_san@kingdee.com>",
        title = "Test begin operation transaction",
        targetClass = "kd.mmc.sfc.opplugin.processplan.ProcessPlanSaveOp",
        targetMethod = "beginoperationtransaction",
        lastUpdateTime = "2025-05-14 14:55:40",
        lastUpdateAuthor = "zhang_san <zhang_san@kingdee.com>",
        methodSignature = "public void beginOperationTransaction(BeginOperationTransactionArgs e)",
        testPoints = {"Functionality"},
        description = "Test begin operation transaction"
    )
    @Test
    @DisplayName("测试校验成功时")
    public void testBeginOperationTransactionValidationSucceeds() {
        //step 创建成功的操作结果
        OperationResult operation = mock(OperationResult.class);
        when(operation.isSuccess()).thenReturn(true);

        // 使用匿名子类注入 Mock 的 OperationResult
        ProcessPlanSaveOp saveOp = new ProcessPlanSaveOp() {
            @Override
            public OperationResult getOperationResult() {
                return operation;
            }
        };
        OperateOption option = OperateOption.create();
        saveOp.setContext(null, null, option);

        //step 执行方法
        saveOp.beginOperationTransaction(beginOperationTransactionArgs);

        //assert 验证 ProcessPickingHelper.setMftStockPlanNo 被调用
        processPickingHelper.verify(
            () -> ProcessPickingHelper.setMftStockPlanNo(any(Map.class)), times(1));
        reset();
    }

    @Test
    @DisplayName("测试校验失败时")
    public void testBeginOperationTransactionValidationFails() {
        //step 创建失败的操作结果
        OperationResult operation = mock(OperationResult.class);
        when(operation.isSuccess()).thenReturn(false);

        ProcessPlanSaveOp saveOp = new ProcessPlanSaveOp() {
            @Override
            public OperationResult getOperationResult() {
                return operation;
            }
        };
        OperateOption option = OperateOption.create();
        saveOp.setContext(null, null, option);

        //step 执行方法，期望抛异常
        Exception exception = Assert.assertThrows(KDBizException.class,
            () -> saveOp.beginOperationTransaction(beginOperationTransactionArgs));

        //assert 验证异常消息
        assertTrue(exception.getMessage().contains("保存校验失败。"));
        reset();
    }

    // ========== 6. try-with-resources 模式（方法级 Mock）==========

    @Test
    public void testBeforeExecuteOperationTransaction() {
        // 使用 try-with-resources 管理仅此方法需要的 Mock
        try (MockedStatic<OperationHelper> operationHelper = mockStatic(OperationHelper.class);
             MockedStatic<ProcessPlanHelper> processPlanHelper = mockStatic(ProcessPlanHelper.class);
             MockedStatic<BusinessDataServiceHelper> businessDataServiceHelper =
                 mockStatic(BusinessDataServiceHelper.class)) {

            //step 数据构造
            operationHelper.when(() -> OperationHelper.setFirstOrLastProcess(any()))
                .thenAnswer(invocation -> null);

            Map<String, Object> validatorResult = new HashMap<>(1);
            validatorResult.put("code", 200L);
            validatorResult.put("message", "errormessage");
            operationHelper.when(() -> OperationHelper.validator(any()))
                .thenReturn(validatorResult);

            processPlanHelper.when(() -> ProcessPlanHelper.sortEntrySetRowSeq(any()))
                .thenAnswer(invocation -> null);

            businessDataServiceHelper.when(() ->
                    BusinessDataServiceHelper.loadFromCache(anyString(), anyString(), any()))
                .thenReturn(new HashMap<>(0));

            // 构建带分录的复杂单据
            DynamicObject entry = new DynamicObjectMocker()
                .add(ProcessPlanConsts.ENTRY.PROCESSSEQUENCE, 1)
                .add(ProcessPlanConsts.ENTRY.PROCESSNUMBER, 10)
                .add("processbarcode", null)
                .add("entryentity_lk", new DynamicObjectMocker()
                    .add("entryentity_lk_stableid", "12345")
                    .add("entryentity_lk_sid", "147258")
                    .add("entryentity_lk_sbillid", "456789").getCollection())
                .getObject();

            DynamicObjectCollection entries = new DynamicObjectCollection();
            entries.add(entry);

            DynamicObject bill = new DynamicObjectMocker()
                .add(ProcessPlanConsts.BILLNO, "GXJH-20251208-00000001")
                .add(ProcessPlanConsts.ENTRY.ENTRYENTITY_ENTITY, entries)
                .add(ProcessPlanConsts.ROOTPROCESSPLAN_ID, 1L)
                .add(ProcessPlanConsts.SPLITLEVEL, 0)
                .getObject();

            BeforeOperationArgs args = mock(BeforeOperationArgs.class);
            when(args.getDataEntities()).thenReturn(new DynamicObject[]{bill});
            when(args.getOperationKey()).thenReturn("save");

            //step 执行方法
            ProcessPlanSaveOp saveOp = new ProcessPlanSaveOp();
            saveOp.beforeExecuteOperationTransaction(args);

            //assert 验证条码已生成
            String barcode = bill.getDynamicObjectCollection(
                ProcessPlanConsts.ENTRY.ENTRYENTITY_ENTITY)
                .get(0).getString("processbarcode");
            assertTrue(barcode != null && !barcode.isEmpty());
            reset();
        }
        // MockedStatic 自动关闭
    }
}
```

## 要点总结

1. **匿名子类注入 OperationResult**：操作插件依赖 `getOperationResult()` 判断成败，
   通过匿名子类 override 注入 Mock 对象
2. **setContext**：操作插件需要调用 `setContext(entityType, operateMeta, option)` 初始化上下文
3. **Mock Args**：`BeginOperationTransactionArgs` 等参数对象用 `mock()` 创建，
   配置 `getDataEntities()` 和 `getOperationKey()` 返回值
4. **try-with-resources**：仅在单个测试方法中使用的 Mock 可用此模式自动管理生命周期
5. **assertThrows**：测试异常场景使用 `Assert.assertThrows(ExceptionClass.class, () -> ...)`
6. **verify 验证调用**：用 `mockStatic.verify(() -> method(), times(n))` 验证静态方法被调用次数
