|
| 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