# Mock 速查：BaseTest 差异对比、常用 Mock 清单、高频混淆场景


本节从原 SKILL.md 拆分而来，集中收纳编写单测时频繁查阅的参考表。


## BaseTest 差异对比

四个工程各有自己的 `BaseTest` / `BasePluginTest`，注意包名和预置 Mock 的差异。

**通用发现规则**：不同模块组的 BaseTest 包名遵循相同规律，**实际使用时建议先读取目标工程的 BaseTest 源码**，确认其预置了哪些 MockedStatic。

| 工程 | BaseTest 包名规律 | 查找路径 |
|--------|--------------|---------------|
| `<模块组>-common` | `kd.<包路径>.common.BaseTest` | `<模块组>-common/src/test/java/.../BaseTest.java` |
| `<模块组>-business` | `kd.<包路径>.business.BaseTest` | `<模块组>-business/src/test/java/.../BaseTest.java` |
| `<模块组>-opplugin` | `kd.<包路径>.opplugin.BaseTest` | `<模块组>-opplugin/src/test/java/.../BaseTest.java` |
| `<模块组>-formplugin` | `kd.<包路径>.formplugin.BasePluginTest` | `<模块组>-formplugin/src/test/java/.../BasePluginTest.java` |

**mmc-sfc 模块已知预置 Mock（作为参考）**：

| 工程 | BaseTest 包名 | 额外预置 Mock |
|--------|--------------|---------------|
| mmc-sfc-common | `kd.mmc.sfc.common.BaseTest` | SaveServiceHelper + EntityMetadataCache |
| mmc-sfc-business | `kd.mmc.sfc.business.BaseTest` | SaveServiceHelper + IDataModel/IFormView（CommonMockObject）；BillUnitAndQtytHelper 仅声明字段未自动初始化 |
| mmc-sfc-opplugin | `kd.mmc.sfc.opplugin.BaseTest` | Singleton + BFTrackerServiceHelper + MutexUtils |
| mmc-sfc-formplugin | `kd.mmc.sfc.formplugin.BasePluginTest` | 表单插件专用参数与 mock（IFormView / IDataModel / AbstractFormPlugin 上下文等），详见 patterns/formplugin.md |

三者（common / business / opplugin）都共同预置了 8 个 MockedStatic：OrgUnitServiceHelper、BillTypeParamHelper、ResManager、SystemParamServiceHelper、QueryServiceHelper、BusinessDataServiceHelper、OperationServiceHelper、BaseDataServiceHelper。其中 common 和 business 额外共享 SaveServiceHelper（opplugin 无此项）。

formplugin 的 BasePluginTest 独立管理，具体预置内容以实际文件为准。

## 常用 Mock 清单

以下是苍穹模块单元测试中高频需要 Mock 的静态类：

| 静态类 | 用途 | 常见 Mock 方式 |
|--------|------|---------------|
| `QueryServiceHelper` | 数据库查询 | `.when(() -> query(...)).thenReturn(...)` |
| `BusinessDataServiceHelper` | 业务数据加载 | `.when(() -> loadSingle/load/loadFromCache(...)).thenReturn(...)` |
| `OperationServiceHelper` | 操作执行 | `.when(() -> executeOperate(...)).thenReturn(opResult)` |
| `SaveServiceHelper` | 数据保存 | `.when(() -> save/update(...)).thenReturn(...)` |
| `BaseDataServiceHelper` | 基础资料 | `.when(() -> getBaseDataFromCache(...)).thenReturn(...)` |
| `OrgUnitServiceHelper` | 组织单元 | 通常直接 mockStatic |
| `SystemParamServiceHelper` | 系统参数 | `.when(() -> getParameterValue(...)).thenReturn(...)` |
| `ResManager` | 多语言资源 | `.when(() -> loadKDString(...)).thenAnswer(inv -> inv.getArgument(0))` |
| `BillTypeParamHelper` | 单据类型参数 | 通常直接 mockStatic |
| `ProcessPlanHelper` | 工序计划业务 | 按需 Mock 具体方法 |
| `AttachmentServiceHelper` | 附件服务 | Mock upload/remove/getAttachments |
| `CacheFactory` | 缓存工厂 | Mock getCommonCacheFactory 链式调用 |
| `BFTrackerServiceHelper` | BOTP 追踪 | 通常直接 mockStatic |
| `EntityMetadataCache` | 元数据缓存 | 通常直接 mockStatic（common 工程常用） |
| `ConvertServiceHelper` | BOTP 转换服务 | Mock push/convert 方法 |
| `ProcessSettleHelper` | 工序结算（含分布式缓存） | 推荐使用 `ProcessSettleMockHelper.mockDistributeSessionlessCache()` 创建，不推荐直接 mockStatic；ProcessSettleMockHelper 在各工程各自 test 目录下查找，找不到则建议告知用户添加（详见 2.7 节） |
| `DB`（kd.bos.db.DB） | 数据库操作 | 建议先执行 ClassPool defrost 处理再 mockStatic（详见 2.6 节） |

## 高频混淆场景

| 场景 | 避免做法 | 推荐做法 |
|------|-----------|-----------|
| 测试 protected 方法 | 改方法可见性为 public | 用 `ReflectHelper.invokeProtectedMethod()` |
| 测试 private static 方法 | 跳过或改可见性 | 用 `ReflectHelper.invokeStaticMethod()` |
| 校验器收集错误消息 | 直接调用 validate 不收集 | 匿名子类重写 `addMessage/addErrorMessage` 收集到 List |
| 构建分录数据 | 手动 new DynamicObject | 用 `DynamicObjectMocker` 链式构造 |
| Mock 静态方法后不关闭 | 忘记 close | @After 中建议 close 所有 MockedStatic |
| ResManager 多语言 | 直接返回空字符串 | thenAnswer 返回第一个参数（保持可读性） |
| POJO 测试用 Mock | 给纯 POJO 加 MockedStatic | 直接 new 对象，无需任何 Mock |
| common 工具类 new 内部对象 | 无法 Mock 构造函数 | 用 `Mockito.mockConstruction()` Mock 构造函数 |
| 断言写法 | `assertTrue(true)` / `assertFalse(false)` 等假断言，或测试方法中根本没有任何 `assert`/`verify` 调用 | 断言被测方法的真实输出或用 `verify()` 校验交互；「提前返回」场景用 `verify(mock, never()).method()` 断言下游调用未被触发 |
| Mockito 重载方法歧义 | `any()` 无法区分 `method(DynamicObject)` 和 `method(List<Long>)` | 显式指定类型：`any(DynamicObject.class)` |
| Mockito varargs 参数匹配 | `verify(view).setEnable(eq(true), any(String[].class))` | `verify(view).setEnable(eq(true), anyString())`（varargs 用 `anyString()` 而非 `any(String[].class)`） |
| `any()` 在非 Object 类型参数上歧义 | `any()` 用于 `Date`/`Set`/`String` 等具体类型参数 | 推荐明确类型：`any(Date.class)`、`any(Set.class)`、`anyString()` 等；`any()` 仅在参数类型为 `Object` 且无重载歧义时使用 |
| Mock 前未确认方法签名 | `anyLong()` 匹配 `String` 参数导致运行时失败 | **编写 mock 前建议读取被测方法的真实签名**，确保 `anyXxx()` 与参数类型一致 |
| `DynamicObjectCollection` 状态控制 | `new DynamicObjectCollection().add(item)` 后 `isEmpty()/size()` 不可靠（框架类需要 DynamicObjectType 初始化） | 当被测代码以 `isEmpty()/size()` 作为分支条件时，改用 `mock(DynamicObjectCollection.class)` 并显式 stub：`when(entries.isEmpty()).thenReturn(false); when(entries.size()).thenReturn(1)` |
| 点分隔字段名的 DynamicObject 访问 | `DynamicObjectMocker.add("entryentity.id", 100L)` 后 `obj.getLong("entryentity.id")` 抛 `ORMDesignException` | `DynamicObjectMocker` 创建的是 Simple 类型实体，点分隔的字段名会被解析为嵌套路径导航。**对于 QueryServiceHelper.query() 平展查询结果中含点号字段的 DynamicObject**，建议使用 `mock(DynamicObject.class)` + `when(obj.getLong/getBigDecimal("key")).thenReturn(value)` |
| Mock DB 类 | 直接 `Mockito.mockStatic(DB.class)` | 建议先执行 ClassPool defrost 处理（详见 2.6 节），否则可能因字节码冻结导致 mock 失败 |
| Mock ProcessSettleHelper | 直接 `Mockito.mockStatic(ProcessSettleHelper.class)` | 推荐使用 `ProcessSettleMockHelper.mockDistributeSessionlessCache()`（详见 2.7 节），在当前工程 test 目录下查找该 Helper，找不到则建议告知用户添加 |
| 常量/枚举类引用不到 | 用字面值 `"A"` 替代 `StatusEnum.APPROVED` | **避免字面值替代**，建议主动告知用户补充常量/枚举类的文件位置或源码 |
| DynamicObject 空值断言 | `assertNull(obj.getString("field"))` | `getString()`、`getLong()`、`getInt()`、`getBigDecimal()`、`getBoolean()` 在苍穹平台默认配置下不返回 null（即使 `set(key, null)`，平台会转为基本值）：空字符串用 `assertEquals("", obj.getString(...))`、空数值用 `assertEquals(0L, obj.getLong(...))`、空 BigDecimal 用 `assertEquals(BigDecimal.ZERO, ...)`、布尔用 `assertFalse(obj.getBoolean(...))`。若被测代码对 getter 结果做了 `null` 检查（如 `if (obj.getBigDecimal("qty") != null)`），说明该字段可能开启了「允许为空」，测试用例建议覆盖 null 和非 null 两个分支；无法判断时默认按非 null 处理，并在测试方法中添加注释 `// 假设字段未开启"允许为空"，若实际可为 null 请告知`。`getDate(key)` 是例外，始终可能为 null，建议用 `assertNull` 或具体日期值断言 |