# 编写测试类 — Mock 与数据构造细节


本节从原 SKILL.md 的「Step 2: 编写测试类」拆分而来，集中说明 DynamicObjectMocker、kd.bos.db.DB 特殊 Mock、ProcessSettleHelper 专用 Helper、MockedConstruction、CommonMockObject 工具、thenAnswer 动态返回模式等可复用片段。


### 2.5 DynamicObjectMocker 用法

```java
// 简单对象
DynamicObject obj = new DynamicObjectMocker()
    .add("id", 1L)
    .add("billno", "TEST-001")
    .add("status", "A")
    .getObject();

// 嵌套对象（基础资料引用）
DynamicObject obj = new DynamicObjectMocker()
    .add("material", new DynamicObjectMocker()
        .add("id", 100L)
        .add("masterid.id", 100L)
        .getObject())
    .add("unit", new DynamicObjectMocker()
        .add("id", 1L)
        .add("precision", 2)
        .getObject())
    .getObject();

// 分录集合
DynamicObject bill = new DynamicObjectMocker()
    .add("id", 1L)
    .add("entryentity", new DynamicObjectMocker()
        .add("id", 10L)
        .add("seq", 1)
        .add("qty", BigDecimal.TEN)
        .getCollection())   // 注意：分录用 getCollection()
    .getObject();

// 多行分录（用于遍历/stream 的场景）
DynamicObjectCollection entries = new DynamicObjectCollection();
entries.add(new DynamicObjectMocker().add("id", 1L).add("qty", BigDecimal.ONE).getObject());
entries.add(new DynamicObjectMocker().add("id", 2L).add("qty", BigDecimal.TEN).getObject());
DynamicObject bill = new DynamicObjectMocker()
    .add("entryentity", entries)
    .getObject();

// 当被测代码用 isEmpty()/size() 作为分支判断时，建议 mock 集合：
// new DynamicObjectCollection().add() 不能保证 isEmpty()/size() 返回正确值
DynamicObjectCollection entries = mock(DynamicObjectCollection.class);
when(entries.isEmpty()).thenReturn(false);
when(entries.size()).thenReturn(2);
```

### 2.6 kd.bos.db.DB 类特殊 Mock

由于 `kd.bos.db.DB` 类的特殊性（字节码冻结问题），对该类的静态方法进行 mock 前，建议先执行 ClassPool defrost 处理：

```java
public class SomeClassTest {

    // 定义变量
    MockedStatic<DB> dbMocked = Mockito.mockStatic(DB.class);

    @Before
    public void before() {
        // 对 DB.class 可能已存在的 mock 执行释放
        try {
            CtClass cls = ClassPool.getDefault().get(DB.class.getName());
            if (cls.isFrozen()) {
                cls.defrost();
            }
        } catch (Exception e) {}
        // 赋值新的 mock
        dbMocked = Mockito.mockStatic(DB.class);
    }

    @After
    public void after() {
        dbMocked.close();
    }
}
```

此模式遵循 2.4 节的 MockedStatic 生命周期规则：多方法使用时定义为成员变量 + @Before/@After 管理。

### 2.7 ProcessSettleHelper 特殊 Mock

由于 `ProcessSettleHelper` 内部使用了苍穹平台分布式缓存，常规 `Mockito.mockStatic()` 无法满足缓存使用需求，建议使用专用的 Helper：

```java
MockedStatic<ProcessSettleHelper> processSettleHelper =
    ProcessSettleMockHelper.mockDistributeSessionlessCache();
```

- `ProcessSettleMockHelper` 是各工程各自在 **test 目录下**提供的辅助类
- 查找策略：在**当前被测类所属工程**的 `src/test/java/` 下搜索 `ProcessSettleMockHelper`
- 若当前工程下找不到该类，**主动告知用户**，由用户在对应工程 test 目录下添加
- 推荐避免固定引用其他工程（如 formplugin）的 ProcessSettleMockHelper

### 2.8 MockedConstruction（Mock 构造函数）

当被测代码内部通过 `new Xxx()` 创建了某个对象，而该对象需要被 mock 时，使用 `Mockito.mockConstruction`：

```java
@Test
public void testMethod() {
    try (MockedConstruction<PushArgs> pushArgsMock =
             Mockito.mockConstruction(PushArgs.class)) {

        //step 执行被测方法（内部会 new PushArgs()）
        SomeUtil.doSomething(params);

        //assert 验证 PushArgs 被正确构造和使用
        assertEquals(1, pushArgsMock.constructed().size());
        PushArgs constructed = pushArgsMock.constructed().get(0);
        verify(constructed).setSomeProperty(any());
    }
}
```

- 适用场景：被测方法内部 `new` 了无法通过参数注入的对象
- 使用 **try-with-resources** 管理生命周期，自动关闭
- 可与 `MockedStatic` 同时使用（在同一个 try 块中声明）

### 2.9 CommonMockObject 工具

`CommonMockObject` 提供了一组通用的 Mock 数据构建工具方法，所有工程均可使用：

| 方法 | 说明 | 适用范围 |
|------|------|--------|
| `CommonMockObject.mockMainEntityType()` | Mock 主实体类型 | 通用 |
| `CommonMockObject.getDynByString(jsonString)` | 从 JSON 字符串构造 DynamicObject | 通用 |
| `CommonMockObject.getDynsByString(jsonString)` | 从 JSON 字符串构造 DynamicObject[] | 通用 |
| `CommonMockObject.getDynMockerByMap(map)` | 从 Map 构造 DynamicObjectMocker（支持嵌套） | 通用 |
| `CommonMockObject.getDynByMap(map)` | 从 Map 构造 DynamicObject | 通用 |
| `CommonMockObject.getTestModel()` | 获取 Mock 的 IDataModel | formplugin（表单插件场景） |
| `CommonMockObject.getTestView()` | 获取 Mock 的 IFormView | formplugin（表单插件场景） |

使用示例：

```java
// 从 JSON 构造 DynamicObject（适用于复杂嵌套数据）
DynamicObject obj = CommonMockObject.getDynByString("{\"id\":1,\"billno\":\"TEST-001\"}");

// 从 Map 构造（适用于需要动态组装字段的场景）
Map<String, Object> map = new HashMap<>();
map.put("id", 1L);
map.put("status", "A");
DynamicObject obj = CommonMockObject.getDynByMap(map);

// Mock 主实体类型（opplugin 场景常用）
CommonMockObject.mockMainEntityType();
```

### 2.10 thenAnswer 动态返回模式

当同一个 Mock 方法被不同参数多次调用，且需要根据入参动态返回不同结果时，使用 `thenAnswer` 代替 `thenReturn`：

```java
// thenReturn：只能返回固定值
queryServiceHelper.when(() -> QueryServiceHelper.query(anyString(), anyString(), any()))
    .thenReturn(fixedResult);

// thenAnswer：根据入参动态返回
queryServiceHelper.when(() -> QueryServiceHelper.query(anyString(), anyString(), any()))
    .thenAnswer(invocation -> {
        String entityName = invocation.getArgument(0);  // 取第1个参数
        if ("mmc_processplan".equals(entityName)) {
            return mockProcessPlanData();   // 查工序计划 -> 返回工序计划数据
        }
        if ("mmc_sendwork".equals(entityName)) {
            return mockSendWorkData();      // 查派工单 -> 返回派工单数据
        }
        return new DynamicObjectCollection();  // 其他 -> 返回空集合
    });
```

适用场景：被测方法内部对同一个 ServiceHelper 发起多次不同参数的调用，每次期望不同结果。