Skip to content

插件测试 ​

给插件写测试,不需要自己拼 setup 的上下文,也不需要 as never。@zhin.js/testing 提供两层工具:

层用途入口
假运行环境(单元测试)快、无需测试应用;直接跑 setup()、命令、中间件、处理器createFakePlugin / runPluginSetup / runCommand …
真实运行时(集成测试)真实的模块发现、命令解析、权限、发送链路PluginTestKit
bash
pnpm add -D @zhin.js/testing

跑 setup ​

假环境用的是真正的 Scope、DisposeStack、GenerationHandoffStack,以及真正的按插件隔离的 databaseHostToken / scheduleHostToken / outboundHostToken 外观,底层换成内存实现。

ts
import { runPluginSetup } from '@zhin.js/testing';
import plugin from '../plugin.js';

it('每 30 秒往目标群发一条消息', async () => {
  const fake = await runPluginSetup(plugin, { config: { target: 'g1' } });

  await fake.clock.advance(65_000);                     // 推进假时钟,到点的 cron 会被执行

  expect(fake.sent.map(call => call.kind)).toEqual(['send', 'send']);
  expect(fake.rows('notes')).toEqual([{ text: 'hello' }]); // setup 里 define 的表
  await fake.dispose();                                  // 触发 lifecycle 里的清理函数
});

createFakePlugin(options) 只创建环境,不跑 setup;之后用 fake.setup(plugin) 运行,或直接把 fake.context 交给自己的函数。

选项作用
name / config插件名与 context.config.get() 的值
resources额外的 token 绑定:[fakeResource(token, value)],类型由 token 决定,无需断言
hosts{ database, schedule, outbound },设为 false 可模拟宿主缺失(配合 resources.has() 的分支)
clock起始时间或共享的 FakeClock
成员说明
fake.context / resources / lifecycle / handoff / signal传给 setup 的各部分
fake.database / schedule / outbound与 resources.use(token) 拿到的是同一个外观
fake.rows(name)读取插件 define(name) 建的表(内存实现,where 只支持等值)
fake.sent记录所有主动发送(send、sendTarget、反应、撤回、编辑、输入中),sendTarget 带发送方插件
fake.outboundHost.failNext(error)让下一次发送失败,用来测失败路径
fake.clockadvance(ms) 推进并按时间顺序执行到期任务;set(time) 只改时间不触发
fake.scheduleHost.trigger(id)立即执行某个已注册任务
fake.featuressetup 里通过 addFeature / addCommand 等登记的能力
fake.missingRequirements(plugin)检查 requires 里有没有没提供的 token

cron 为六段(秒 分 时 日 月 周),支持 *、*/n、列表和范围,按本地时间计算。database.state 的 TTL 使用真实的 Date.now(),不受假时钟影响。

跑命令、中间件、处理器 ​

约定目录里的定义可以直接拿来执行,上下文由假环境构造:

ts
import { createFakeMessage, createFakePlugin, fakeResource, runCommand, runMiddleware, runHandler } from '@zhin.js/testing';
import checkin from '../commands/checkin/index.js';

const fake = createFakePlugin({ resources: [fakeResource(greeterToken, greeter)] });
const message = createFakeMessage({ sender: { id: 'u1', name: 'Ann', roles: ['admin'] } });

await runCommand(fake, checkin, { input: message, params: { days: 3 } });
expect(message.replies).toEqual(['签到成功']);          // $reply 被记录在 replies

const { continued } = await runMiddleware(fake, gate, createFakeMessage({ content: 'skip' }));
expect(continued).toBe(false);                          // 中间件是否调用了 next()

await runHandler(fake, onReceive, createFakeMessage()); // this 即能力上下文

这不是完整的派发

runCommand 直接调用 execute():不做路由匹配、参数解析、permit 校验和准入检查,params 需要你自己给出已解析的值。要验证这些,用下面的真实运行时。

局部假对象:fakeOf ​

内部协作者或事件载荷只需要几个成员时,用 fakeOf<T>({...}) 代替 {...} as never——强制转换集中在这一处,调用处仍由 Partial<T> 检查成员名和类型:

ts
const orchestrator = fakeOf<ActivityFeedbackOrchestrator>({ startPhase: vi.fn(), stopPhase: vi.fn() });

有真实或套件自带的假对象时优先用它们,fakeOf 只用于窄协作者。

真实运行时:PluginTestKit ​

假环境不经过渲染器、出站中间件和端点协商。需要验证"消息真的按这条链路发出去"时,用 PluginTestKit:它挂载一个一次性的真实测试应用,合成端点记录最终载荷。用法与限制见套件 README。

kit.messageDiagnostics 与 Console GET /api/messages/diagnostics 读取的是同一份有界、无正文的诊断,可以断言每条消息的路由、派发状态和投递结果。

官方示例 ​

plugins/services/activity-feedback/tests/activity-feedback-runtime.test.ts 已迁移到这套工具:setup 用 createFakePlugin 提供事件总线和宿主,载荷与编排器用 fakeOf,文件内不再有 as never。

下一步 ​