# FormPlugin 表单插件测试模式

## 适用范围

`<模块组>-formplugin` 工程中继承以下基类的插件类：
- `kd.bos.form.plugin.AbstractFormPlugin`（表单插件）
- `kd.bos.list.plugin.AbstractListPlugin`（列表插件）
- `kd.bos.bill.plugin.AbstractBillPlugin`（单据插件）

这些插件在表单/列表/单据生命周期（打开、值更新、按钮点击等）的各阶段介入处理。

## 测试文件位置

```
<模块组>-formplugin/src/test/java/kd/<包路径>/formplugin/{subpackage}/XxxPluginTest.java
```

与被测类保持相同的包路径。

---

## 基类与参数获取

### 优先级链

formplugin 测试的基类和参数获取遵循以下优先级：

1. **优先从 BasePluginTest 获取**
   - 在 `<模块组>-formplugin/src/test/java/` 下搜索 `BasePluginTest`
   - 该基类封装了 AbstractFormPlugin 测试所需的通用参数和 mock 取値逻辑（如 IFormView、IDataModel、表单上下文等）

2. **参考同包下已有测试类**
   - 路径：`<模块组>-formplugin/src/test/java/kd/<包路径>/formplugin/` 及其子包
   - 查找以 `Test` 结尾的类，参考其中的类似写法

3. **降级到苍穹公共单测框架**
   - 若以上均无可参考案例，使用苍穹公共单测框架
   - **建议主动告知用户**缺失的取值逻辑，由用户补充到 BasePluginTest

### 使用方式

```java
package kd.<包路径>.formplugin.subpackage;

// 继承 BasePluginTest 获取通用 mock 和参数
public class XxxPluginTest extends BasePluginTest<XxxPlugin> {
    // BasePluginTest 已预置的 mock 和参数可直接使用
    // 额外需要的 mock 在此声明
}
```

---

## 表单插件生命周期方法

| 方法 | 触发时机 | 测试重点 |
|------|---------|---------|
| `afterCreateNewData` | 新建单据后 | 字段默认值设置 |
| `afterLoadData` | 数据加载后 | 界面控件状态设置 |
| `afterBindData` | 数据绑定后 | 控件可见性/可编辑性 |
| `beforeDoOperation` | 操作执行前 | 前置校验逻辑 |
| `propertyChanged` | 字段值变更 | 联动逻辑（switch/if 分支） |
| `itemClick` | 按钮/菜单点击 | 按钮响应逻辑 |
| `beforeClosed` | 表单关闭前 | 清理逻辑 |
| `closedCallBack` | 子表单关闭回调 | 回调处理逻辑 |

---

## 模板结构

```java
package kd.<包路径>.formplugin.subpackage;

import kd.bos.dataentity.entity.DynamicObject;
import kd.bos.form.plugin.AbstractFormPlugin;
import kd.bos.test.ext.annotaions.UnittestCaseInfo;
import kd.bos.form.unittest.DisplayName;
import kd.bos.unittest.mock.DynamicObjectMocker;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

public class XxxPluginTest extends BasePluginTest<XxxPlugin> {

    // 1. 声明额外需要 Mock 的静态类（BasePluginTest 未覆盖的）
    private MockedStatic<SomeHelper> someHelper;

    @Before
    public void before() {
        // 若 BasePluginTest 有 @Before，调用 super.before()
        someHelper = Mockito.mockStatic(SomeHelper.class);
    }

    @After
    public void after() {
        // 若 BasePluginTest 有 @After，调用 super.after()
        someHelper.close();
    }

    // ========== propertyChanged 方法测试（switch 分支拆分）==========

    @UnittestCaseInfo(
        author = "<name> <<name>@kingdee.com>",
        title = "Test propertyChanged when field is materialId",
        targetClass = "kd.<包路径>.formplugin.subpackage.XxxPlugin",
        targetMethod = "propertychanged",
        lastUpdateTime = "yyyy-MM-dd HH:mm:ss",
        lastUpdateAuthor = "<name> <<name>@kingdee.com>",
        methodSignature = "public void propertyChanged(PropertyChangedArgs e)",
        testPoints = {"Functionality"},
        description = "Test propertyChanged switch case: materialId"
    )
    @Test
    @DisplayName("propertyChanged - switch case: materialId 字段变更")
    public void testPropertyChanged_CaseMaterialId() {
        //step 构建 PropertyChangedArgs，fieldKey = "materialId"
        // ... 使用 BasePluginTest 提供的方式构建参数

        //step 执行被测方法
        XxxPlugin plugin = new XxxPlugin();
        // ... 设置插件上下文
        // plugin.propertyChanged(args);

        //assert 验证联动结果
        // assertEquals(expectedValue, ...);
    }

    @UnittestCaseInfo(
        author = "<name> <<name>@kingdee.com>",
        title = "Test propertyChanged when field is qty",
        targetClass = "kd.<包路径>.formplugin.subpackage.XxxPlugin",
        targetMethod = "propertychanged",
        lastUpdateTime = "yyyy-MM-dd HH:mm:ss",
        lastUpdateAuthor = "<name> <<name>@kingdee.com>",
        methodSignature = "public void propertyChanged(PropertyChangedArgs e)",
        testPoints = {"Functionality"},
        description = "Test propertyChanged switch case: qty"
    )
    @Test
    @DisplayName("propertyChanged - switch case: qty 字段变更")
    public void testPropertyChanged_CaseQty() {
        //step 构建 PropertyChangedArgs，fieldKey = "qty"
        // ...

        //step 执行被测方法
        // ...

        //assert 验证联动结果
        // ...
    }

    // ========== 判断分支覆盖（笛卡尔积）==========

    @Test
    @DisplayName("beforeDoOperation - 条件组合覆盖")
    public void testBeforeDoOperation_ConditionCombinations() {
        //step 条件组合1: status=A, hasEntry=true -> 允许操作
        // ...
        //assert
        // ...

        //step 条件组合2: status=A, hasEntry=false -> 阻止操作
        // ...
        //assert
        // ...

        //step 条件组合3: status=B, hasEntry=true -> 阻止操作
        // ...
        //assert
        // ...

        //step 条件组合4: status=B, hasEntry=false -> 阻止操作
        // ...
        //assert
        // ...
    }
}
```

---

## 特殊 Mock 要求

> **DB 类 ClassPool defrost** 和 **ProcessSettleMockHelper** 是所有工程通用的特殊 Mock 规则，详见 SKILL.md 2.6 / 2.7 节。此处不再重复，formplugin 工程同样遵循。

### MockedStatic 生命周期规则

| 场景 | 方式 |
|------|------|
| 同一静态类在 > 1 个测试方法中使用 | 定义为**成员变量**，@Before 赋值，@After close |
| 同一静态类仅在 1 个测试方法中使用 | 使用 **try-with-resources** 局部 mock |

---

## DynamicObject 断言规范

对 DynamicObject 取值进行断言时的默认值规则：

| 取值方法 | 默认值（字段未赋值时） | 断言写法 |
|---------|---------------------|---------|
| `getString(key)` | `""` (空字符串，不为 null) | `assertEquals("", obj.getString("field"))` |
| `getLong(key)` | `0L` | `assertEquals(0L, obj.getLong("field"))` |
| `getInt(key)` | `0` | `assertEquals(0, obj.getInt("field"))` |
| `getBigDecimal(key)` | `BigDecimal.ZERO` | `assertEquals(BigDecimal.ZERO, obj.getBigDecimal("field"))` |
| `getBoolean(key)` | `false` | `assertFalse(obj.getBoolean("field"))` |
| `getDate(key)` | **可能为 null** | `assertNull(obj.getDate("field"))` 或具体日期值 |

**原则：除 getDate 外，DynamicObject 的 getter 方法默认不返回 null，使用基本值断言。**

---

## 常量/枚举类引用规则

- 测试代码中引用常量、枚举时，**推荐使用原始常量类/枚举类的引用**（如 `StatusEnum.APPROVED`），而非字面值（如 `"A"`）
- 若常量类/枚举类**无法找到**（如未在 classpath 中、编译报错等），**建议主动告知用户**，由用户补充：
  - 文件路径
  - 或完整源码
- **推荐避免**忽略使用逻辑或用字面值替代

---

## 关键要点总结

1. **基类优先级**：BasePluginTest -> 同包 *Test 类参考 -> 苍穹公共框架（缺失告知用户）
2. **switch 推荐拆方法**：propertyChanged / itemClick 等含 switch 的方法，每个 case 独立测试方法
3. **判断分支笛卡尔积**：if/else 条件组合需全面覆盖，可合并到一个方法中
4. **DB 类 / ProcessSettleMockHelper**：通用规则，详见 SKILL.md 2.6 / 2.7 节
5. **DynamicObject 断言**：getString/getLong/getBigDecimal 等用基本值断言，getDate 除外
6. **常量/枚举**：避免字面值替代，找不到就建议告知用户
