Skip to content

Commit c453546

Browse files
committed
[docs] Move mocking to plugins/mock
1 parent ea000c2 commit c453546

4 files changed

Lines changed: 215 additions & 156 deletions

File tree

docs/content/docs/development/meta.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"protocol",
77
"plugin",
88
"logging",
9-
"mocking",
109
"digging"
1110
]
1211
}

docs/content/docs/development/mocking.mdx

Lines changed: 0 additions & 154 deletions
This file was deleted.

docs/content/docs/development/plugin.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ npx tsx smoke-test.ts
281281

282282
### Mock 测试 (Mock Test)
283283

284-
使用 `node:test` 等测试框架并且结合 [`@fraqjs/mock`](./mocking.mdx) 来分单元、分场景测试插件中的单个功能是否按照预期工作。`@fraqjs/mock` 是一个用于模拟 Milky 客户端的库,并且包含了模拟消息发送、API Stub 等功能
284+
在编写机器人时,我们不一定总是拥有 / 愿意运行一个完整的 Milky 协议端来测试我们的代码,在这种情况下,创建一个模拟(Mock)环境是非常有用的。[`@fraqjs/plugin-mock`](../plugins/mock.mdx) 提供了这样的功能:它以一个 `MockService` 拦截所有出站 API 调用并注入事件,让你可以在没有真实协议端的情况下驱动插件、检查其行为
285285

286286
对于单元测试,建议将测试文件命名为 `*.test.ts`,并且放在 `test` 目录下。你可以在 `package.json` 中添加一个 `test` script 来运行你的测试,例如:
287287

docs/content/docs/plugins/mock.mdx

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
---
2+
title: '@fraqjs/plugin-mock'
3+
---
4+
5+
<NpmBadge packageName="@fraqjs/plugin-mock" />
6+
7+
`@fraqjs/plugin-mock` 提供了模拟 Milky 协议端的能力,帮助开发者编写插件测试。
8+
9+
## 安装与配置
10+
11+
与其他插件不同,`@fraqjs/plugin-mock` 只在测试时使用,不需要在 `fraq.yml` 中加载,也不会随机器人一起运行。因此请将它作为开发依赖安装:
12+
13+
```bash
14+
npm install -D @fraqjs/plugin-mock
15+
# or
16+
yarn add -D @fraqjs/plugin-mock
17+
# or
18+
pnpm add -D @fraqjs/plugin-mock
19+
```
20+
21+
如果你是插件开发者,为了在自己插件的测试中使用它,同样将它添加到项目的 `devDependencies` 中即可。本插件的核心是 `MockService`,你既可以让下面介绍的 `createMockContext` 帮你装配好一切,也可以手动安装 `MockPlugin`,并在其他插件中通过 `inject` 声明对它的依赖:
22+
23+
```typescript
24+
import { MockService } from '@fraqjs/plugin-mock';
25+
26+
definePlugin({
27+
name: 'my-plugin',
28+
inject: {
29+
mock: MockService,
30+
},
31+
apply(ctx) {
32+
// 使用 ctx.mock 来访问 MockService
33+
},
34+
});
35+
```
36+
37+
## 初始化 Mock 环境
38+
39+
最直接的方式是使用 `createMockContext` 函数,它会创建一个已经装配好 `MockService``Context`,你可以通过 `ctx.mock` 访问这个服务:
40+
41+
```typescript
42+
import { createMockContext } from '@fraqjs/plugin-mock';
43+
44+
const ctx = createMockContext();
45+
ctx.install(EchoPlugin);
46+
await ctx.start();
47+
48+
// 通过 ctx.mock 驱动测试
49+
await ctx.mock.receiveFriend({ userId: 10001 }, inmsg`ping`);
50+
```
51+
52+
`createMockContext` 默认使用一个直接打印到命令行、没有任何颜色样式的 `LogHandler`,你也可以通过 `logHandler` 选项传入其他的日志处理器(例如 [`@fraqjs/color-log`](../development/logging.mdx#使用-color-log) 提供的处理器),方便插件的调试。它还接受 `MockService` 的全部配置项(如 `selfId``baseTime` 等)。
53+
54+
### 在已有 Context 上安装
55+
56+
如果你已经有一个 `Context`,也可以直接安装 `MockPlugin`。它会拦截该 Context 的所有 API 调用并注入事件,你可以通过 `ctx.resolve(MockService)` 取回服务实例:
57+
58+
```typescript
59+
import { MockPlugin, MockService } from '@fraqjs/plugin-mock';
60+
61+
ctx.install(MockPlugin);
62+
ctx.install(PluginUnderTest);
63+
await ctx.start();
64+
65+
const mock = ctx.resolve(MockService);
66+
```
67+
68+
请务必在被测插件**之前**安装 `MockPlugin`,以确保 API 拦截在被测插件运行前就位。其他插件也可以通过 `inject: { mock: MockService }` 来依赖它。
69+
70+
## 模拟消息事件
71+
72+
`MockService` 提供了一些函数来模拟不同类型的消息事件:
73+
74+
```typescript
75+
await ctx.mock.receiveFriend({ userId: 10001 }, inmsg`ping`);
76+
await ctx.mock.receiveGroup({ groupId: 20001, userId: 10001 }, inmsg`/deploy`);
77+
await ctx.mock.receiveTemp({ userId: 10001, groupId: 20001 }, inmsg`hello`);
78+
```
79+
80+
第一个参数包含了消息事件的相关信息,例如发送者的 QQ 号、所在的群号等,你也可以提供一些其他的信息来覆盖默认值,例如:
81+
82+
```typescript
83+
await ctx.mock.receiveFriend(
84+
{
85+
userId: 10001,
86+
peerId: 10001,
87+
senderId: 10001,
88+
messageSeq: 7,
89+
time: 123456,
90+
},
91+
inmsg`hello`,
92+
);
93+
```
94+
95+
第二个参数是一个 `IncomingSegment[]`,你可以使用 `inmsg` 来方便地创建它。`inmsg` 同样支持字符串、数字、布尔值插值、`inseg` 插值、自动 `trim`,用法与之前提到的 [`msg`](../development/message.mdx#msg-函数)[`seg`](../development/message.mdx#seg-命名空间) 一致;不同的地方在于 `inmsg``inseg` 生成的是 `IncomingSegment`
96+
97+
## 生成实体信息
98+
99+
`@fraqjs/plugin-mock` 还提供了生成好友、群聊以及群成员信息的函数。这些函数将提供的 ID 作为 [PCG32](https://zh.wikipedia.org/wiki/%E7%BD%AE%E6%8D%A2%E5%90%8C%E4%BD%99%E7%94%9F%E6%88%90%E5%99%A8)`seed`,并且从内置的词库中选取片段来组合实体信息,因此生成的信息是**可复现**的。上面提到的模拟消息事件函数会自动调用这些生成函数来生成相关的实体信息,你也可以直接调用它们来获取这些信息:
100+
101+
```typescript
102+
createRandomFriend(10001);
103+
createRandomGroup(20001);
104+
createRandomGroupMember(20001, 10001);
105+
```
106+
107+
你也可以提供一些覆盖默认值的选项:
108+
109+
```typescript
110+
createRandomFriend(10001, { remark: 'Override' });
111+
createRandomGroup(20001, { group_name: 'Override' });
112+
```
113+
114+
## 处理 API 调用
115+
116+
`MockService` 默认会拦截所有出站 API 调用。对于与实体、消息读取有关的 API,它会从内置的 `MockInbox` 中生成合理的响应;对于其他 API(例如各类发送消息的 API),它默认返回一个空对象 `{}`,这足以让只解构响应中个别字段(如 `message_seq`)的调用方正常工作。
117+
118+
如果你需要为某个 API 端点设置自定义的响应,请使用 `Context` 提供的 `hookApi` 方法。这里有两种用法:
119+
120+
**替换响应**:不调用 `next`,直接返回一个响应。此时该 API 调用会被这个 hook 完全接管,不会再流向 `MockService`,因此也不会被记录到 `apiCalls` 中。
121+
122+
```typescript
123+
ctx.hookApi('get_friend_info', (params) => ({
124+
friend: {
125+
user_id: params.user_id,
126+
nickname: 'Override',
127+
sex: 'unknown',
128+
qid: 'qid_override',
129+
remark: '',
130+
category: {
131+
category_id: 1,
132+
category_name: 'General',
133+
},
134+
},
135+
}));
136+
```
137+
138+
**观察或改写响应**:调用 `next` 让请求继续流向 `MockService`,再对其返回值进行处理。此时该调用仍会被 `MockService` 记录。
139+
140+
```typescript
141+
ctx.hookApi('send_private_message', async (params, next) => {
142+
const result = await next();
143+
return { ...result, message_seq: 42 };
144+
});
145+
```
146+
147+
### 内置响应
148+
149+
`MockService` 会自动为下面这些 API 端点提供响应,无需额外设置。
150+
151+
首先是一些与消息有关的 API,它们的响应会根据提供的信息从已经发送过的消息中生成:
152+
153+
- `get_message`
154+
- `get_history_messages`
155+
- `mark_message_as_read`
156+
157+
除此之外,还有一些与实体信息有关的 API。它们会先寻找之前模拟消息事件中包含的实体信息,如果未发现匹配的实体信息,则会调用前面提到的生成实体信息的函数来生成一个新的实体信息作为响应:
158+
159+
- `get_friend_info`
160+
- `get_group_info`
161+
- `get_group_member_info`
162+
163+
如果要覆盖这些 API 的默认响应,直接使用上面介绍的 `hookApi` 方法即可。
164+
165+
## 模拟其他事件
166+
167+
`MockService` 还提供了一个 `emitEvent` 方法来模拟其他类型的事件,例如:
168+
169+
```typescript
170+
await ctx.mock.emitEvent({
171+
event_type: 'group_join_request',
172+
time: 123456,
173+
self_id: ctx.mock.inbox.selfId,
174+
data: {
175+
group_id: 20001,
176+
notification_seq: 7,
177+
is_filtered: false,
178+
initiator_id: 10001,
179+
comment: `
180+
问题:从哪了解到本群的
181+
答案:GitHub
182+
`.trim(),
183+
},
184+
});
185+
```
186+
187+
## 完整示例
188+
189+
所有刚才的介绍都只涉及了我们该如何模拟用户输入,但更重要的部分在于检查插件对这些输入的响应。`MockService` 提供了一个名叫 `apiCalls` 的属性,来记录由 `Context` 发起的所有 API 调用。每当插件调用一个 API 时,`apiCalls` 中就会记录下这个调用的端点和参数,你可以检查 `apiCalls` 的最后若干条记录来验证插件是否正确地调用了预期的 API,以及调用时使用了正确的参数。
190+
191+
例如,仍然以我们的 Echo 机器人为例,让我们编写一个完整的测试:
192+
193+
```typescript
194+
import assert from 'node:assert/strict';
195+
import test from 'node:test';
196+
197+
import { createMockContext, inmsg } from '@fraqjs/plugin-mock';
198+
199+
test('echo plugin echoes string input prefixed with echo', async () => {
200+
const ctx = createMockContext();
201+
ctx.install(EchoPlugin);
202+
await ctx.start();
203+
204+
await ctx.mock.receiveFriend({ userId: 10001 }, inmsg`echo Hello`);
205+
206+
assert.deepEqual(ctx.mock.apiCalls.at(-1), {
207+
endpoint: 'send_private_message',
208+
params: {
209+
user_id: 10001,
210+
message: [{ type: 'text', data: { text: 'Hello' } }],
211+
},
212+
});
213+
});
214+
```

0 commit comments

Comments
 (0)