---
name: cosmic-unittest
description: |
  苍穹模块单元测试编写技能 — 适用于任何遵循 common / business / opplugin / formplugin 四工程结构的苍穹模块。
  当用户说"写单测"、"写单元测试"、"写测试"、"写测试用例"、"补单测"、"补测试"、"加测试"、"生成测试"、
  "unittest"、"unit test"、"test case"，或打开苍穹模块的 Java 源码/测试文件并要求编写测试时触发。
---

# 苍穹模块单元测试编写技能

## 适用产品线

- 适用：基于 Cosmic/BOS Java 插件模型的金蝶AI苍穹、金蝶AI星瀚、金蝶AI套件单元测试。
- 不适用：企业版 C# / IronPython 插件测试。

## 技能说明

本技能适用于任何遵循「四工程结构」的金蝶AI苍穹模块，包括但不限于：
- **mmc-sfc**（SFC 车间管理）、**bd-mpdm**（生产数据管理）、**sys-xkbase** 等各业务模块

四大工程角色通用定义：
- **`<模块组>-common`**：公共模块（常量类 / 枚举类 / POJO / 工具类）
- **`<模块组>-business`**：静态帮助类 / 业务逻辑
- **`<模块组>-opplugin`**：操作插件（Validator / OpPlugin / ConvertPlugin）
- **`<模块组>-formplugin`**：表单插件（继承 AbstractFormPlugin / AbstractListPlugin / AbstractBillPlugin 的子类）

> `<模块组>` 是被测类所在模块的前缀，需在 Step 0 中自动识别（如 `mmc-sfc`、`bd-mpdm`）。

---

## 目录结构

```
cosmic-unittest/
├── SKILL.md                    ← 本文件：技能入口与决策树
├── references/
│   ├── author-info.md          ← Author 信息获取（缓存策略、git config 读取、.gitignore 检查）
│   ├── analysis-output.md      ← 分析结果输出与 Bug 检查（结构化摘要模板、常见 Bug 应对）
│   ├── writing-test-class.md   ← 编写测试类 — Mock 与数据构造细节（DynamicObjectMocker / DB / ProcessSettleHelper / MockedConstruction / CommonMockObject / thenAnswer）
│   └── mock-catalog.md         ← Mock 速查（BaseTest 差异对比 + 常用 Mock 清单 + 高频混淆场景）
├── patterns/
│   ├── common-module.md        ← 公共模块测试模式（POJO / 枚举 / 工具类）
│   ├── business-helper.md      ← 业务帮助类测试模式
│   ├── validator.md            ← 校验器测试模式
│   ├── op-plugin.md            ← 操作插件测试模式
│   ├── convert-plugin.md       ← 转换插件测试模式
│   └── formplugin.md           ← 表单插件测试模式（formplugin 工程专用）
└── examples/
    ├── common-test.md          ← 完整 common 测试示例（POJO / 枚举 / 工具类）
    ├── business-test.md        ← 完整 business 测试示例
    ├── validator-test.md       ← 完整 validator 测试示例
    ├── op-plugin-test.md       ← 完整 op-plugin 测试示例
    └── formplugin-test.md      ← 完整 formplugin 测试示例（BasePluginTest<T> 继承模式）
```

按需加载原则：阅读本 SKILL.md 的决策树定位到目标 pattern/examples 后，再展开对应 references 子节；不要一次性全量加载所有文件。

---

## Step 0: 判断测试类型

### Step 0.0: 识别模块组前缀

**首先**从被测类的包名或文件路径中提取**模块组前缀**：

```
包名示例                           对应模块组前缀
kd.mmc.sfc.business.XxxHelper   →  mmc-sfc
kd.bd.mpdm.business.XxxHelper   →  bd-mpdm
kd.sys.xkbase.business.XxxHelper →  sys-xkbase
```

识别后将前缀统一称为 `<模块组>`，后续所有工程名、包名、路径均基于此进行推导。

### Step 0.1: 根据工程类型决定测试模式

### 按需加载 pattern 文件（性能优化）

根据 Step 0.1 识别出的工程类型，**推荐只读取对应的 1 个 pattern 文件**，避免全量加载所有 pattern 导致上下文膨胀：

| 工程类型 | 读取的 pattern 文件 | 按需参考的 example 文件 |
|---------|-------------------|---------------------|
| common - POJO/DTO | `patterns/common-module.md` #POJO 段 | `examples/common-test.md` POJO 段 |
| common - 枚举 | `patterns/common-module.md` #Enum 段 | `examples/common-test.md` Enum 段 |
| common - 工具类 | `patterns/common-module.md` #Utils 段 | `examples/common-test.md` Utils 段 |
| business | `patterns/business-helper.md` | `examples/business-test.md` |
| opplugin - Validator | `patterns/validator.md` | `examples/validator-test.md` |
| opplugin - OpPlugin | `patterns/op-plugin.md` | `examples/op-plugin-test.md` |
| opplugin - ConvertPlugin | `patterns/convert-plugin.md` | `examples/op-plugin-test.md`（参考结构） |
| formplugin | `patterns/formplugin.md` | `examples/formplugin-test.md` |

> example 文件仅在需要参考具体写法时读取，非必读。

### Step 0.2: 批量测试识别（同工程多个类）

若用户一次提交了**同一工程下多个被测类**，采用批量模式提升效率：

```
批量模式流程
│
├─ Step 1：并行读取所有被测类源码，统一完成分析（含 Bug 检查）
├─ Step 2：跳过分析摘要确认（遵循用户偏好），直接生成代码
├─ Step 3：依次生成所有测试类代码（可并行生成互不依赖的测试类）
└─ Step 4：全部生成完成后，执行一次 Gradle 运行，统一验证
```

> 批量模式的核心价值：① **Gradle 只冷启动一次**；② **文件读取并行化**（所有被测类源码同一轮并行读取）；③ **测试类生成可并行**（同工程不同被测类的测试类互不依赖）。

### 批量模式并行策略（性能优化）

批量模式下，各步骤的并行执行策略：

| 步骤 | 并行策略 | 说明 |
|------|---------|------|
| 源码读取 | 所有被测类 + 所有常量类 + BaseTest 同一轮并行读取 | 减少多轮串行等待 |
| 逻辑分析 | 逐类分析，但汇总输出一次 | 批量模式仍跳过确认环节 |
| 代码生成 | 互不依赖的测试类可并行生成 | 同工程的 BaseTest 共享，减少重复读取 |
| Gradle 验证 | 只执行一次 `gradle :<模块组>-<工程>:test` | 所有测试类合并一次运行 |

收到用户「写单元测试」请求时，根据被测类所在工程决定测试模式：

```
被测类在哪个工程?
│
├─ <模块组>-common（公共模块：常量 / 枚举 / POJO / 工具类）
│   ├─ POJO / DTO / Bean 类（纯 getter/setter）
│   │   → 使用 POJO 测试模式（读 patterns/common-module.md #POJO）
│   │   → 无需 Mock，无需基类，直接 new 对象测试
│   │
│   ├─ 枚举类（含业务方法如 getByCode / match）
│   │   → 使用 Enum 测试模式（读 patterns/common-module.md #Enum）
│   │   → 继承 AbstractJunitNoDependenciesTest
│   │
│   └─ 工具类（XxxUtil / XxxUtils，含静态方法）
│       → 使用 Common Utils 测试模式（读 patterns/common-module.md #Utils）
│       → 继承当前模块 common 工程的 BaseTest 或 AbstractJunitNoDependenciesResManagerTest
│   → 测试文件放在: <模块组>-common/src/test/java/...
│
├─ <模块组>-business（XxxHelper / XxxService 等静态工具类）
│   → 使用 Business Helper 测试模式（读 patterns/business-helper.md）
│   → 测试文件放在: <模块组>-business/src/test/java/...
│
├─ <模块组>-opplugin
│   ├─ Validator 目录下的校验器类
│   │   → 使用 Validator 测试模式（读 patterns/validator.md）
│   │
│   ├─ 继承 AbstractOperationServicePlugIn 的操作插件
│   │   → 使用 OpPlugin 测试模式（读 patterns/op-plugin.md）
│   │
│   └─ ConvertPlugin / BotpPlugin 等转换插件
│       → 使用 ConvertPlugin 测试模式（读 patterns/convert-plugin.md）
│   → 测试文件放在: <模块组>-opplugin/src/test/java/...
│
└─ <模块组>-formplugin（表单插件，继承 AbstractFormPlugin / AbstractListPlugin / AbstractBillPlugin 的子类）
    → 使用 FormPlugin 测试模式（读 patterns/formplugin.md）
    → 基类与参数获取优先级：
       1. 优先在 <模块组>-formplugin/src/test/java/ 下搜索 BasePluginTest 获取测试所需参数及 mock 取值逻辑
       2. 若 BasePluginTest 中未定义相关逻辑，参考同包及子包下 *Test 结尾类的类似写法
       3. 若以上均无可参考案例，尝试使用苍穹公共单测框架，并**主动告知用户**缺失的取值逻辑，由用户补充到 BasePluginTest
    → 测试文件放在: <模块组>-formplugin/src/test/java/... （与被测类保持相同包路径）
```
---

## Step 0.5: 获取 Author 信息

> 详细缓存策略、git config 读取顺序、`.gitignore` 检查提示见 `references/author-info.md`。

简要流程：

1. **优先级 1**：读取 `.qoder/skills/cosmic-unittest/author-cache.json`，若 `author` 非空则直接使用，跳过 git config。
2. **优先级 2**：执行 `git config user.name` / `git config user.email`，按 `"<name> <<name>@kingdee.com>"` 格式拼接后写入缓存。
3. **优先级 3**：若 git config 失败，询问用户并写入缓存。
4. 首次写入后检查 `.gitignore` 是否忽略该缓存文件，若未忽略提示用户添加。
5. `lastUpdateTime` = 用例编写时的当前时间，格式 `yyyy-MM-dd HH:mm:ss`。

---

## Step 1: 分析被测类

### 快速模式判断（POJO / 枚举 → 跳过分析摘要）

若被测类属于以下**极简场景**，**直接跳过「被测逻辑分析」摘要输出，立即生成测试代码**：

| 极简场景 | 判断依据 |
|---------|--------|
| **POJO / DTO / Bean** | 类中只有字段 + getter/setter/构造方法，无业务逻辑方法 |
| **纯枚举类** | 仅含枚举值定义，无自定义业务方法（如 `getByCode` / `match`）；或只有一个简单的 `getByCode` 静态方法 |

> 对于上述场景，无需 Mock、无分支、无依赖，分析摘要价值极低，直接生成代码效率更高。

---

其余类型（工具类 / Business Helper / Validator / OpPlugin / FormPlugin）按完整流程执行：

1. 读取被测类的完整源码
2. 识别所有 public / protected 方法
3. 识别被测方法中调用的**静态方法**（这些需要 MockedStatic）
4. 识别被测方法中依赖的**外部服务**（QueryServiceHelper、BusinessDataServiceHelper 等）
5. 识别方法参数中的 **DynamicObject** 结构（需要 DynamicObjectMocker 构造）
6. **识别被测方法中的 switch 分支和判断分支**（用于 Step 3 设计测试方法拆分策略）
7. **检查被测代码是否存在 Bug**（见下方说明）

### 并行全量读取（性能优化）

读取被测类源码后，**立即将以下所有文件读取并行发起**，在同一轮 tool calls 中一次性拿齐，不得串行逐个等待：

| 读取目标 | 搜索/读取方式 | 说明 |
|---------|-------------|------|
| 常量类 / 枚举类 | 从 import 提取 `Const/Consts/Enum` 结尾的类名，grep_code 并行搜索 | 搜索实际使用的字段值 |
| BaseTest 源码 | search_file 搜索 `BaseTest.java` 或 `BasePluginTest.java` | 确认预置 MockedStatic |
| 对应 pattern 文件 | 直接 read_file（Step 0.1 已确定文件名） | 若 Step 0 未读取则在此补读 |
| 被测方法调用的关联类 | 从 import 提取同模块的 Helper/Service 类 | 读取方法签名 |

```
示例（一轮并行 tool calls）：
1. grep_code → ProcessPlanConsts 中被测方法使用的字段
2. grep_code → SendWorkConsts 中被测方法使用的字段
3. search_file → BaseTest.java
4. read_file → patterns/op-plugin.md
5. grep_code → BillUnitAndQtytHelper 方法签名
```

> 本次优化来源：v1.3 已实现常量并行读取，v1.4 将其扩展为「全量并行」，
> 将 BaseTest、pattern、关联类的读取也纳入同一批次，减少多轮串行等待。

### 智能 Mock 推断（性能优化）

从被测类的 import 语句中，**自动推断**需要 Mock 的静态类，与「常用 Mock 清单」对照匹配：

```
推断规则：
1. 扫描被测类所有 import，提取「常用 Mock 清单」中出现的类
2. 将这些类标记为「候选 Mock 类」
3. 结合 BaseTest 已预置的 MockedStatic 进行去重（详见优化4）
4. 仅对「候选 Mock 类 - BaseTest 已预置」的差集声明新的 MockedStatic
```

**常见 import → Mock 映射表**：

| import 中的类 | 需 Mock 的方式 |
|-------------|-------------|
| `kd.bos.servicehelper.QueryServiceHelper` | `mockStatic` + `.when(() -> query(...))` |
| `kd.bos.servicehelper.BusinessDataServiceHelper` | `mockStatic` + `.when(() -> loadSingle/load/loadFromCache(...))` |
| `kd.bos.servicehelper.OperationServiceHelper` | `mockStatic` + `.when(() -> executeOperate(...))` |
| `kd.bos.servicehelper.SaveServiceHelper` | `mockStatic` + `.when(() -> save/update(...))` |
| `kd.bos.servicehelper.base.BaseDataServiceHelper` | `mockStatic` + `.when(() -> getBaseDataFromCache(...))` |
| `kd.bos.org.service.OrgUnitServiceHelper` | `mockStatic`（通常无需 stub 方法） |
| `kd.bos.servicehelper.SystemParamServiceHelper` | `mockStatic` + `.when(() -> getParameterValue(...))` |
| `kd.bos.resmanager.ResourceManager` | `mockStatic` + `.when(() -> loadKDString(...)).thenAnswer(inv -> inv.getArgument(0))` |
| `kd.bos.servicehelper.BillTypeParamHelper` | `mockStatic`（通常无需 stub 方法） |
| `kd.bos.db.DB` | `mockStatic`（推荐先 ClassPool defrost 以避开字节码冻结，详见 2.6 节） |
| `kd.mmc.sfc.helper.ProcessSettleHelper` | 避免直接 mockStatic，推荐使用 `ProcessSettleMockHelper` 缓存管理，详见 2.7 节 |
| `kd.bos.cache.CacheFactory` | `mockStatic` + 链式 stub |

> 该映射表替代人工逐个判断「这个类需不需要 Mock」，将依赖识别从经验驱动变为规则驱动。

### Bug 检查要求与分析结果输出格式

> 详见 `references/analysis-output.md`，包含：Bug 应对策略表、5 类常见 Bug（边界判断、空指针、逻辑取反、数值计算、集合判断）的应对方式、结构化「被测逻辑分析」摘要模板。
---

## Step 2: 编写测试类

### 2.1 通用规则

| 规则 | 说明 |
|------|------|
| 测试框架 | JUnit 4（@Test / @Before / @After） |
| Mock 框架 | Mockito 2.x + MockedStatic |
| **Mockito 静态导入** | 推荐避免使用 `import static org.mockito.ArgumentMatchers.*` 和 `import static org.mockito.Mockito.*` 通配符导入以防命名冲突；推荐显式逐个导入，标准模板：`any` / `anyBoolean` / `anyInt` / `anyLong` / `anyString` / `eq` / `isNull`（ArgumentMatchers），`atLeastOnce` / `mock` / `never` / `verify` / `when`（Mockito），按需单独添加其他方法 |
| 数据构造 | `DynamicObjectMocker`（链式 `.add(key, value)` 构造 DynamicObject） |
| 基类选择 | 无 ResManager 依赖 → `AbstractJunitNoDependenciesTest`；有 ResManager → `AbstractJunitNoDependenciesResManagerTest` |
| 反射工具 | `ReflectHelper.invokeProtectedMethod()` 测试 protected 方法；`ReflectHelper.invokeStaticMethod()` 测试 private static 方法 |
| 资源释放 | 所有 MockedStatic 推荐在 `@After` 中调用 `.close()` 释放以防状态污染 |
| 注解 | 每个 @Test 方法推荐添加 `@UnittestCaseInfo` 和 `@DisplayName` 以明确用例归属和意图 |
| 断言质量 | **断言质量偏好**：① 推荐避免出现 `assertTrue(true)` 等无实质验证意义的断言；② 推荐每个 @Test 方法至少包含一条有效的 `assert` 或 `verify` 调用，避免无断言测试方法无法真正覆盖缺陷。对于「提前返回」场景，建议使用 `verify(mock, never()).method()` 验证下游调用未被触发 |
| DynamicObject 断言 | 对 DynamicObject 取值断言时，除 `getDate(...)` 外（可能为 null），`getString()`、`getBigDecimal()`、`getLong()` 等默认不会为 null，使用基本值判定：空字符串用 `assertEquals("", obj.getString(...))`、空数值用 `assertEquals(0L, obj.getLong(...))`、空 BigDecimal 用 `assertEquals(BigDecimal.ZERO, obj.getBigDecimal(...))` |
| switch 分支策略 | 被测方法中的 **switch 分支**，推荐将每个 case 拆为**独立的测试方法**；case 内部的多个场景可以在同一方法中测试（也可根据复杂程度进一步拆分） |
| 判断分支覆盖 | 被测方法（含关联调用的其他方法）中的 **if/else 判断分支**，需尽量覆盖所有条件组合（笛卡尔积），可将不同条件取值合并到同一个测试方法中 |
| 常量/枚举引用 | 若引用的常量类、枚举类无法找到，建议主动告知用户由其补充；推荐避免使用字面值硬编码替代常量引用 |

### BaseTest Mock 继承去重（性能优化）

测试类继承 BaseTest 后，BaseTest 已预置的 MockedStatic **不得重复声明**。按以下规则去重：

```
去重流程：
1. 读取 BaseTest 源码，提取所有 @Before 中初始化的 MockedStatic 字段
2. 在「智能 Mock 推断」产出的「候选 Mock 类」中，减去 BaseTest 已预置的类
3. 测试类仅需声明「差集」对应的 MockedStatic 成员变量
4. 在 @Before 中仅初始化差集的 MockedStatic
5. 在 @After 中仅关闭差集的 MockedStatic
```

**典型场景示例**（business 工程）：

```java
// BaseTest 已预置：QueryServiceHelper、BusinessDataServiceHelper 等 8+2 个
// 被测类还需要：ProcessPlanHelper（不在 BaseTest 中）

public class SomeHelperTest extends BaseTest {
    // 仅声明 BaseTest 未预置的
    MockedStatic<ProcessPlanHelper> processPlanHelper;

    @Before
    public void before() {
        super.before();  // 触发 BaseTest 的预置 Mock
        processPlanHelper = Mockito.mockStatic(ProcessPlanHelper.class);
    }

    @After
    public void after() {
        processPlanHelper.close();
        super.after();  // BaseTest 关闭其预置 Mock
    }
}
```

> 去重的好处：① 减少 MockedStatic 声明和初始化代码量；② 避免 BaseTest 与测试类对同一类重复 mock 导致冲突；
> ③ 测试类代码更简洁，关注点集中在业务特有的 Mock 上。

### 2.2 @UnittestCaseInfo 注解规范

```java
@UnittestCaseInfo(
    author = "<name> <<name>@kingdee.com>",          // 格式：git config user.name 拼接邮箱，如 "zhang_san <zhang_san@kingdee.com>"
    title = "英文标题（简短描述测试目的）",
    targetClass = "被测类全限定名",
    targetMethod = "被测方法名（小写）",
    lastUpdateTime = "yyyy-MM-dd HH:mm:ss",          // 用例编写时的当前时间
    lastUpdateAuthor = "<name> <<name>@kingdee.com>", // 格式同 author
    methodSignature = "被测方法完整签名",
    testPoints = {"Functionality"},
    description = "英文描述"
)
```

### 2.3 测试方法注释规范

```java
@Test
@DisplayName("中文描述：测试场景说明")
public void testMethodName_ScenarioDescription() {
    //step 准备测试数据 / 构建 Mock
    ...
    //step 执行被测方法
    ...
    //assert 断言结果
    ...
    reset();  // 如有 reset 方法
}
```

### 2.4 MockedStatic 生命周期

```java
// 声明为成员变量
MockedStatic<XxxServiceHelper> xxxServiceHelper;

@Before
public void before() {
    xxxServiceHelper = Mockito.mockStatic(XxxServiceHelper.class);
    // 配置通用返回值...
}

@After
public void after() {
    xxxServiceHelper.close();  // 推荐及时关闭释放以防影响后续测试
}

public void reset() {
    xxxServiceHelper.reset();  // 在测试方法末尾重置状态
}
```

### 2.5-2.10: 数据构造与高级 Mock 模式

> 详见 `references/writing-test-class.md`，包含：

- **2.5 DynamicObjectMocker 用法**：嵌套对象 / 分录集合 / 多行分录 / `mock(DynamicObjectCollection.class)` 替代方案
- **2.6 kd.bos.db.DB 特殊 Mock**：ClassPool defrost 处理模板
- **2.7 ProcessSettleHelper 特殊 Mock**：`ProcessSettleMockHelper.mockDistributeSessionlessCache()` 模式与查找策略
- **2.8 MockedConstruction**：mock 构造函数场景的 try-with-resources 模板
- **2.9 CommonMockObject 工具**：mockMainEntityType / getDynByString / getDynByMap / getTestModel / getTestView
- **2.10 thenAnswer 动态返回模式**：根据入参返回不同结果

---

## Step 3: 选择测试场景

为每个被测方法设计以下场景：

| 场景类型 | 说明 | 示例 |
|---------|------|------|
| 正常路径 | 标准输入，预期正常输出 | 数据完整时校验通过 |
| 边界条件 | 空值 / 空集合 / 零值 | 分录为空、数量为0 |
| **边界值（重点）** | **比较运算符的临界点，推荐覆盖「刚好满足」和「刚好不满足」两侧以测出边界缺陷** | 见下表 |
| 异常路径 | 异常输入或依赖服务异常 | 查询返回 null、抛异常 |
| 业务规则 | 特定业务条件分支 | 单据状态不同的处理逻辑 |

### 边界值覆盖要求

**推荐为每个比较运算符设计恰好覆盖两侧的测试用例：**

| 比较运算符 | 推荐覆盖的值 | 示例（可退回数量校验） |
|-----------|------------|---------------------|
| `> 0`（大于零） | 等于0（不通过）、大于0（通过） | qty=0 → 校验失败；qty=0.01 → 校验通过 |
| `>= 0`（大于等于零） | 等于0（通过）、小于0（不通过） | qty=0 → 通过；qty=-0.01 → 失败 |
| `a.compareTo(b) > 0` | a==b（不通过）、a>b（通过）、a<b（不通过） | rejectQty=canRejectQty（边界相等）、超出（失败）、未超出（通过） |
| `a.compareTo(b) >= 0` | a<b（不通过）、a==b（通过）、a>b（通过） | 同上，覆盖相等时通过的情况 |
| `size() > N` | size==N（不通过）、size==N+1（通过） | 分录恰好N条 vs N+1条 |
| `isEmpty()` | 空集合、恰好1条、多条 | entryIdSet 为空 / 1个 / 多个 |

**原则：临界值（boundary value）推荐在测试用例中覆盖，避免只测「远离边界」的正常值。**

### switch / 判断分支拆分策略

| 分支类型 | 测试方法拆分规则 | 示例 |
|---------|---------------|------|
| **switch 分支** | 每个 case 推荐定义为独立的测试方法 | `testPropertyChanged_CaseFieldA()`、`testPropertyChanged_CaseFieldB()` |
| switch case 内部多场景 | 允许在同一方法中测试多个场景（过于复杂时可进一步拆分） | 一个 case 内有 3 个 if 分支，可在一个方法中覆盖 |
| **if/else 判断分支** | 需覆盖所有条件组合（笛卡尔积），可合并到同一测试方法 | 2 个布尔条件 → 4 种组合，可用 4 组数据在一个方法中测试 |
| 被测方法调用的其他方法中的分支 | 同上，关联方法的判断分支也需要覆盖 | 被测方法内部调用了 `checkStatus()`，该方法的分支也需覆盖 |

---

## Step 4: 验证与运行

测试类编写完成后，执行以下检查闭环：

1. **代码静态分析门控**（建议优先修复静态分析中发现的问题，再运行 Gradle 任务）
   - 确认 import 语句完整
   - 确认所有 MockedStatic 都在 @After 中关闭
   - 确认测试方法命名清晰（testXxx_Scenario 格式）
   - 确认常量类/枚举类引用正确（非字面値替代）
   - 确认 DynamicObject 断言使用基本値（非 null 判定）
   - 确认每个 @Test 方法均有有效的 assert 或 verify 调用
   - 确认 BaseTest 已预置的 MockedStatic 未重复声明
   - 确认 MockedStatic 初始化顺序正确（super.before() 在前，自有 Mock 在后）
   - 若上述任意自检项不符合，推荐先修复，再执行第 2 步

2. **运行测试用例**

   ### Gradle 运行前环境快速判断（跳过无效探测）

   **在运行 Gradle 前，先执行以下两项快速检查，任一失败则直接跳过 Gradle，提示用户在 IDE 中运行：**

   | 检查项 | 检查方式 | 失败时处理 |
   |--------|---------|----------|
   | ① biz lib 中是否存在被测模块的 jar | `ls <biz路径>/<模块组>-formplugin*.jar`（或对应工程） | 不存在 → 跳过 Gradle，提示「请先在 IDE 中 Build 工程或运行 copytolib，再通过 IDE 运行测试」 |
   | ② config.gradle 中的 biz 路径在本机是否真实存在 | 读取 `config.gradle` 中的 `biz` 路径，执行 `ls <biz路径>` | 路径不存在 → 跳过 Gradle，提示「config.gradle 路径与本机不符，请在 IDE 中运行测试」 |

   > 本次优化来源：InsideAcceptEditPlugin 测试中，花费 ~8 分钟探测 gradle 可执行文件位置、
   > 尝试构建依赖链，最终因 mmc-sfc-common.jar 未构建而失败，属于完全可预判的无效耗时。

   通过环境判断后，运行 Gradle test 任务验证（根据识别到的模块组前缀替换 `<模块组>`）：
   `gradle :<模块组>-common:test` / `gradle :<模块组>-business:test` / `gradle :<模块组>-opplugin:test` / `gradle :<模块组>-formplugin:test`
   例如 mmc-sfc 模块：`gradle :mmc-sfc-common:test` / `gradle :mmc-sfc-business:test` / ...

3. **错误修复**
   - 对出现的错误内容进行修复（如不完善的 mock 导致取值报错、断言与实际结果不匹配等）
   - 修复后**重新运行**验证，直到用例全部通过
   - 输出最终结果

---

## 参考资料

> 以下三节的完整内容已迁移到 `references/mock-catalog.md`，本文件不再展开，避免重复维护：

- **BaseTest 差异对比**：四个工程的 BaseTest / BasePluginTest 包名规律 + mmc-sfc 模块已知预置 Mock 清单
- **常用 Mock 清单**：苍穹模块高频静态类的 Mock 方式（QueryServiceHelper / BusinessDataServiceHelper / OperationServiceHelper / SaveServiceHelper / DB / ProcessSettleHelper 等）
- **高频混淆场景**：常见易错点（protected/private 反射、MockedStatic 关闭、`.any()` 重载歧义、`.anyString()` vs `.any(String[].class)`、DynamicObject 空值断言、点分隔字段名 ORMDesignException 等）
