技能执行流程
Step 0.0
识别模块组
从包名推导前缀
→
Step 0.1
判断测试类型
工程 → 模式
→
Step 0.5
获取 Author
缓存 → git → 询问
→
Step 1
分析被测类
依赖 + Bug 审查
→
Step 2
编写测试类
注解 + Mock + 断言
→
Step 3
覆盖场景
边界值 + 分支
→
Step 4
验证运行
静态分析 + Gradle
v1.4 性能优化专区
按需加载 pattern 文件
根据工程类型推荐只读取 1 个 pattern,避免全量加载:
| 工程类型 | pattern 文件 |
|---|---|
| common - POJO | common-module.md #POJO |
| common - 枚举 | common-module.md #Enum |
| common - 工具类 | common-module.md #Utils |
| business | business-helper.md |
| opplugin - Validator | validator.md |
| opplugin - OpPlugin | op-plugin.md |
| opplugin - Convert | convert-plugin.md |
| formplugin | formplugin.md |
example 文件仅在需要参考具体写法时按需读取,非必读。
并行全量读取
读取被测类源码后,以下全部同一轮并行发起:
常量/枚举类 import 提取 → grep_code 并行搜索
BaseTest 源码 search_file 搜索确认预置 Mock
对应 pattern 直接 read_file 补读
关联类签名 import 提取 → grep_code 搜索方法签名
v1.3 只并行读取常量,v1.4 扩展为全量并行,减少多轮串行等待。
智能 Mock 推断
从 import 自动匹配映射表,规则驱动取代经验驱动:
| import 中的类 | Mock 方式 |
|---|---|
QueryServiceHelper | mockStatic + when(query) |
BusinessDataServiceHelper | mockStatic + when(load...) |
OperationServiceHelper | mockStatic + when(executeOperate) |
SaveServiceHelper | mockStatic + when(save/update) |
OrgUnitServiceHelper | mockStatic(通常无需 stub) |
ResManager | mockStatic + thenAnswer(getArg(0)) |
DB | 推荐先 ClassPool defrost |
ProcessSettleHelper | 推荐避免直接 mockStatic,用 MockHelper |
BaseTest Mock 继承去重
仅声明 BaseTest 未预置的 MockedStatic 差集:
public class SomeHelperTest extends BaseTest { // 仅声明 BaseTest 未预置的 MockedStatic<ProcessPlanHelper> planHelper; @Before public void before() { super.before(); // 先触发 BaseTest planHelper = mockStatic(ProcessPlanHelper.class); } @After public void after() { planHelper.close(); super.after(); } }
批量模式并行策略
同工程多被测类,最大化并行:
| 步骤 | 并行策略 |
|---|---|
| 源码读取 | 所有被测类 + 常量 + BaseTest 同一轮并行 |
| 逻辑分析 | 逐类分析,汇总输出一次(跳过确认) |
| 代码生成 | 互不依赖的测试类可并行生成 |
| Gradle 验证 | 只冷启动一次,合并运行 |
核心价值:Gradle 冷启动一次 + 文件读取并行化 + 测试类并行生成
Step 0.0 — 识别模块组前缀
从被测类的包名或文件路径中提取 <模块组>,后续所有工程名、包名、路径均基于此推导。
| 包名示例 | 模块组前缀 |
|---|---|
kd.mmc.sfc.business.XxxHelper | mmc-sfc |
kd.bd.mpdm.business.XxxHelper | bd-mpdm |
kd.sys.xkbase.business.XxxHelper | sys-xkbase |
kd.mmc.pom.business.XxxHelper | mmc-pom |
Step 0.1 — 工程 → 测试模式
<模块组>-common 公共模块
● POJO/DTO/Bean → POJO 模式 无需 Mock 直接 new
● 枚举类(含业务方法)→ Enum 模式 继承 AbstractJunitNoDependenciesTest
● 工具类 XxxUtil/XxxUtils → Utils 模式 继承 BaseTest
<模块组>-business 业务帮助类
<模块组>-opplugin 操作插件工程
● Validator → Validator 模式
● AbstractOperationServicePlugIn → OpPlugin 模式
● ConvertPlugin/BotpPlugin → ConvertPlugin 模式
<模块组>-formplugin 表单插件工程
Step 0.5 — 获取 Author 信息(三级优先级)
缓存文件 — 读取
.qoder/skills/cosmic-unittest/author-cache.json,若 author 非空直接使用,跳过 git 命令git config — 缓存不存在或 author 为空时,执行
git config user.name + git config user.email,读取成功后写入缓存询问用户 — git config 读取失败时,主动询问用户提供姓名和邮箱,填入缓存
格式规范:
例如:
<name> <<name>@kingdee.com>例如:
zhang_san <zhang_san@kingdee.com>
首次写入
author-cache.json 后,检查 .gitignore 是否已包含该文件的忽略规则,避免个人信息提交到 git。
Step 1 — 分析被测类
读取完整源码 — 识别所有 public/protected 方法
依赖识别 — 静态方法(MockedStatic)、外部服务、DynamicObject 结构
分支识别 — switch 分支 / if-else 判断分支(用于 Step 3 拆分策略)
Bug 审查 — 边界方向错误、空指针风险、逻辑取反、计算错误、集合遗漏
v1.4 并行全量读取 — 常量类 + BaseTest + pattern + 关联类方法签名,同一轮并行拿齐
v1.4 智能 Mock 推断 — 从 import 自动匹配「import → Mock 映射表」,规则驱动取代经验驱动
分析结果:分析完成后推荐输出「被测逻辑分析」摘要(含 Bug 检查结论),推荐避免跳过直接给代码。
发现 Bug → 测试断言反映正确预期(让测试失败暴露 Bug)+ 注释
发现 Bug → 测试断言反映正确预期(让测试失败暴露 Bug)+ 注释
// [警告] BUG: ... + 告知用户。
Step 2 — 编写测试类
2.1 通用编写规则
| 规则 | 说明 |
|---|---|
| 测试框架 | JUnit 4(@Test / @Before / @After) |
| Mock 框架 | Mockito 2.x + MockedStatic |
| Mockito 静态导入 | [错误] 避免使用通配符 import static ...ArgumentMatchers.*[通过] 推荐显式逐个导入: any / anyBoolean / anyInt / anyLong / anyString / eq / isNull(ArgumentMatchers);atLeastOnce / mock / never / verify / when(Mockito) |
| 数据构造 | DynamicObjectMocker 链式 .add(key, value) |
| 基类选择 | 无 ResManager → AbstractJunitNoDependenciesTest有 ResManager → AbstractJunitNoDependenciesResManagerTest多依赖 → 继承工程 BaseTest |
| 反射工具 | ReflectHelper.invokeProtectedMethod()ReflectHelper.invokeStaticMethod() |
| 资源释放 | 所有 MockedStatic 推荐在 @After 中 .close() |
| 注解要求 | 每个 @Test 方法推荐加 @UnittestCaseInfo 和 @DisplayName |
| 断言质量 | [错误] 避免假断言(assertTrue(true) 等)[错误] 避免无断言测试方法 [通过] 每个 @Test 至少一条有效 assert/verify |
| switch 策略 | 每个 case 推荐拆为独立测试方法 |
| if/else 覆盖 | 尽量覆盖所有条件组合(笛卡尔积),可合并到同一方法 |
| 常量/枚举引用 | 找不到时主动告知用户,推荐避免字面值替代 |
| BaseTest Mock 去重 | 不得重复声明 BaseTest 已预置的 MockedStatic,仅声明「候选 Mock 类 - BaseTest 已预置」的差集 @Before 中先 super.before() 再初始化自有 Mock |
2.2 @UnittestCaseInfo 注解规范
@UnittestCaseInfo( author = "<name> <<name>@kingdee.com>", // git config user.name 拼接邮箱 title = "英文标题(简短描述测试目的)", targetClass = "被测类全限定名", targetMethod = "被测方法名(小写)", lastUpdateTime = "yyyy-MM-dd HH:mm:ss", lastUpdateAuthor = "<name> <<name>@kingdee.com>", methodSignature = "被测方法完整签名", testPoints = {"Functionality"}, description = "英文描述" )
2.3 测试方法注释规范
@Test @DisplayName("中文描述:测试场景说明") public void testMethodName_ScenarioDescription() { //step 准备测试数据 / 构建 Mock ... //step 执行被测方法 ... //assert 断言结果 ... reset(); // 如有 reset 方法 }
DisplayName 来自 kd.bos.form.unittest.DisplayName(非 org.junit.DisplayName)
2.4–2.5 MockedStatic & DynamicObjectMocker
MockedStatic 生命周期
// 成员变量声明 MockedStatic<XxxHelper> xxxHelper; @Before public void before() { xxxHelper = mockStatic(XxxHelper.class); } @After public void after() { xxxHelper.close(); // 推荐及时关闭! } public void reset() { xxxHelper.reset(); }
DynamicObjectMocker 链式构造
// 简单对象 DynamicObject obj = new DynamicObjectMocker() .add("id", 1L) .add("billno", "TEST-001") .getObject(); // 分录集合(用 getCollection) DynamicObject bill = new DynamicObjectMocker() .add("entryentity", new DynamicObjectMocker() .add("qty", BigDecimal.TEN) .getCollection()) .getObject();
[警告] 2.6 DB 类特殊 Mock(ClassPool Defrost)
DB 类存在字节码冻结问题,mock 前推荐先执行 defrost 处理:
@Before public void before() { try { CtClass cls = ClassPool.getDefault() .get(DB.class.getName()); if (cls.isFrozen()) { cls.defrost(); } } catch (Exception e) {} dbMocked = Mockito.mockStatic(DB.class); }
[警告] 2.7 ProcessSettleHelper 特殊 Mock
含分布式缓存,不可直接 mockStatic:
MockedStatic<ProcessSettleHelper> mock = ProcessSettleMockHelper .mockDistributeSessionlessCache();
在当前被测类所属工程的
src/test/java/ 下查找 ProcessSettleMockHelper。找不到则主动告知用户在对应工程 test 目录下添加。
2.8–2.10 高级 Mock 技巧
MockedConstruction(Mock 构造函数)
@Test public void testMethod() { try (MockedConstruction<PushArgs> m = Mockito.mockConstruction(PushArgs.class)) { SomeUtil.doSomething(params); assertEquals(1, m.constructed().size()); } // 自动关闭 }
thenAnswer 动态返回
helper.when(() -> QueryServiceHelper .query(anyString(), anyString(), any())) .thenAnswer(inv -> { String entity = inv.getArgument(0); if ("mmc_plan".equals(entity)) return mockPlanData(); return new DynamicObjectCollection(); });
DynamicObject 断言规范
| 方法 | 默认值 | 断言 |
|---|---|---|
getString() | "" | assertEquals("", ...) |
getLong() | 0L | assertEquals(0L, ...) |
getBigDecimal() | ZERO | assertEquals(ZERO, ...) |
getBoolean() | false | assertFalse(...) |
getDate() | 可能 null | assertNull(...) |
Step 3 — 测试场景设计
场景类型与边界值要求
| 场景类型 | 说明 |
|---|---|
| 正常路径 | 标准输入,预期正常输出 |
| 边界条件 | 空值 / 空集合 / 零值 |
| ★ 边界值(重点) | 比较运算符临界点,推荐覆盖「刚好满足」和「刚好不满足」两侧 |
| 异常路径 | 查询返回 null、抛异常 |
| 业务规则 | 特定业务条件分支 |
边界值覆盖要求
| 运算符 | 推荐覆盖的值 |
|---|---|
> 0 | 等于0(不通过)、大于0(通过) |
>= 0 | 等于0(通过)、小于0(不通过) |
compareTo > 0 | a==b(不通过)、a>b(通过)、a<b(不通过) |
size() > N | size==N(不通过)、size==N+1(通过) |
isEmpty() | 空集合、恰好1条、多条 |
switch / if-else 分支拆分策略
| 分支类型 | 拆分规则 |
|---|---|
| switch 分支 | 每个 case 推荐定义为独立测试方法testPropertyChanged_CaseFieldA() |
| switch case 内部多场景 | 允许在同一方法中测试(过复杂时可进一步拆分) |
| if/else 判断 | 覆盖所有条件组合(笛卡尔积),可合并到同一方法 |
| 关联方法的分支 | 被测方法调用的其他方法中的分支也需覆盖 |
Step 4 — 验证与运行
静态分析 import 完整 / MockedStatic 全部关闭 / 方法命名清晰 / 常量引用正确 / BaseTest 未重复声明(v1.4) / super.before() 在前(v1.4)
Gradle 运行前快速判断 — 先检查 ① biz lib 中被测模块 jar 是否存在 ② config.gradle 中 biz 路径是否真实存在;任一失败 → 直接跳过 Gradle,提示用户 IDE 运行(节省 ~8min)
Gradle 运行 (将
<模块组> 替换为实际前缀)gradle :<模块组>-common:test
gradle :<模块组>-business:test
gradle :<模块组>-opplugin:test
gradle :<模块组>-formplugin:test
# 例如 mmc-sfc 模块:
gradle :mmc-sfc-business:test
BaseTest 差异对比
通用 BaseTest 发现规则
实际使用时推荐先读取目标工程的 BaseTest 源码,确认其预置了哪些 MockedStatic。
| 工程 | BaseTest 包名规律 | 查找路径 |
|---|---|---|
<模块组>-common | kd.<包路径>.common.BaseTest | <模块组>-common/src/test/.../BaseTest.java |
<模块组>-business | kd.<包路径>.business.BaseTest | <模块组>-business/src/test/.../BaseTest.java |
<模块组>-opplugin | kd.<包路径>.opplugin.BaseTest | <模块组>-opplugin/src/test/.../BaseTest.java |
<模块组>-formplugin | kd.<包路径>.formplugin.BasePluginTest | <模块组>-formplugin/src/test/.../BasePluginTest.java |
mmc-sfc 模块已知预置 Mock(参考)
三个工程(common/business/opplugin)共同预置 8 个 MockedStatic:OrgUnitServiceHelper、BillTypeParamHelper、ResManager、SystemParamServiceHelper、QueryServiceHelper、BusinessDataServiceHelper、OperationServiceHelper、BaseDataServiceHelper。
| 工程 | 额外预置 Mock |
|---|---|
| mmc-sfc-common | SaveServiceHelper + EntityMetadataCache |
| mmc-sfc-business | SaveServiceHelper + IDataModel/IFormView;BillUnitAndQtytHelper(仅声明未初始化) |
| mmc-sfc-opplugin | Singleton + BFTrackerServiceHelper + MutexUtils |
| mmc-sfc-formplugin | BasePluginTest<T> 独立管理,以实际文件为准 |
常用 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(...) | — |
ResManager | 多语言资源 | .thenAnswer(inv -> inv.getArgument(0)) | — |
EntityMetadataCache | 元数据缓存 | 直接 mockStatic | common 工程常用 |
CacheFactory | 缓存工厂 | Mock getCommonCacheFactory 链式调用 | — |
ConvertServiceHelper | BOTP 转换 | Mock push/convert 方法 | — |
DB(kd.bos.db.DB) | 数据库操作 | mockStatic | 推荐先 ClassPool defrost(见 2.6) |
ProcessSettleHelper | 工序结算 | ProcessSettleMockHelper.mockDistributeSessionlessCache() | 推荐避免直接 mockStatic(见 2.7) |
高频混淆场景速查
| 场景 | [错误] 错误做法 | [通过] 正确做法 |
|---|---|---|
| 测试 protected 方法 | 改方法可见性为 public | ReflectHelper.invokeProtectedMethod() |
| 测试 private static 方法 | 跳过或改可见性 | ReflectHelper.invokeStaticMethod() |
| 校验器收集错误 | 直接 validate 不收集 | 匿名子类重写 addMessage/addErrorMessage 收集到 List |
| POJO 测试用 Mock | 给纯 POJO 加 MockedStatic | 直接 new 对象,无需任何 Mock |
| 断言写法 | assertTrue(true) 假断言 / 测试方法无任何 assert | 断言真实输出或 verify() 交互;提前返回用 verify(mock, never()).method() |
| Mockito 重载歧义 | any() 无法区分重载 | 显式指定 any(DynamicObject.class) |
| any() 类型歧义 | any() 用于 Date/Set/String 等具体类型参数 | 推荐明确类型:any(Date.class)、any(Set.class)、anyString();any() 仅在参数类型为 Object 且无歧义时使用 |
| varargs 参数匹配 | any(String[].class) | anyString()(varargs 专用) |
| DynamicObjectCollection 状态 | new DynamicObjectCollection().add() 后 isEmpty/size 不可靠 | mock(DynamicObjectCollection.class) + 显式 stub |
| 点分隔字段名 | DynamicObjectMocker.add("a.b", v) 后 getLong("a.b") 抛 ORMDesignException | 平展查询结果用 mock(DynamicObject.class) + when().thenReturn() |
| Mock DB 类 | 直接 mockStatic(DB.class) | 先执行 ClassPool defrost(见 2.6) |
| 常量/枚举类引用不到 | 用字面值 "A" 替代 | 推荐避免字面值替代,主动告知用户补充 |
| DynamicObject 空值断言 | assertNull(obj.getString("field")) | getString()/getLong()/getInt()/getBigDecimal()/getBoolean() 平台默认不返回 null(即使 set 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 或具体日期值断言。 |
| DynamicObjectMocker + .set() | 被测代码调 dataEntity.set(field, v),dataEntity 由 DynamicObjectMocker 创建 | 改用 mock(DynamicObject.class) + stub,.set() 在 mock 对象上是空操作,不抛 ORMDesignException |
各测试模式快速参考
common 模块 — POJO 模式
public class XxxModelTest { private XxxModel model; @Before public void setUp() { model = new XxxModel(); } @UnittestCaseInfo(author = "...", ...) @Test public void testGetSetFieldName() { model.setFieldName("value"); assertEquals("value", model.getFieldName()); } @Test public void testDefaultValues() { // 输出字段默认为 ZERO,输入字段默认 null assertEquals(BigDecimal.ZERO, model.getAmount()); assertNull(model.getFormId()); } }
business 模块 — Helper 模式
public class XxxHelperTest extends AbstractJunitNoDependenciesResManagerTest { MockedStatic<QueryServiceHelper> qsh; @Before public void before() { qsh = mockStatic(QueryServiceHelper.class); } @After public void after() { qsh.close(); } @UnittestCaseInfo(...) @Test @DisplayName("正常路径") public void testMethod_NormalCase() { qsh.when(() -> QueryServiceHelper.query(...)) .thenReturn(mockData); Object result = XxxHelper.method(param); assertNotNull(result); reset(); } }
opplugin — Validator 模式
public class XxxValidatorTest extends AbstractJunitNoDependenciesResManagerTest { private final List<String> errors = new ArrayList<>(); private XxxValidator buildValidator() { return new XxxValidator() { @Override protected void addErrorMessage( ExtendedDataEntity e, String msg) { errors.add(msg); } }; } @Test public void testValidate_ShouldPass() { XxxValidator v = buildValidator(); v.setDataEntities(buildData()); v.validate(); assertEquals(0, errors.size()); } }
formplugin — FormPlugin 模式
public class XxxPluginTest extends BasePluginTest<XxxPlugin> { private MockedStatic<SomeHelper> someHelper; @Before public void before() { someHelper = mockStatic(SomeHelper.class); } @After public void after() { someHelper.close(); } // switch 每个 case 独立方法 @Test public void testPropertyChanged_CaseMaterialId() { ... } @Test public void testPropertyChanged_CaseQty() { ... } }
cosmic-unittest v1.4 · 适用于 common / business / opplugin / formplugin 四工程结构的苍穹模块 · 6 项性能优化