Fraqv0.17.0

@fraqjs/plugin-mock

@fraqjs/plugin-mock npm version

@fraqjs/plugin-mock 提供了模拟 Milky 协议端的能力,帮助开发者编写插件测试。

安装与配置

与其他插件不同,@fraqjs/plugin-mock 只在测试时使用,不需要在 fraq.yml 中加载,也不会随机器人一起运行。因此请将它作为开发依赖安装:

npm install -D @fraqjs/plugin-mock
# or
yarn add -D @fraqjs/plugin-mock
# or
pnpm add -D @fraqjs/plugin-mock

如果你是插件开发者,为了在自己插件的测试中使用它,同样将它添加到项目的 devDependencies 中即可。本插件的核心是 MockService,你既可以让下面介绍的 createMockContext 帮你装配好一切,也可以手动安装 MockPlugin,并在其他插件中通过 inject 声明对它的依赖:

import { definePlugin } from '@fraqjs/fraq';
import { MockService } from '@fraqjs/plugin-mock';

definePlugin({
  name: 'my-plugin',
  inject: {
    mock: MockService,
  },
  apply(ctx) {
    // 使用 ctx.mock 来访问 MockService
  },
});

初始化 Mock 环境

最直接的方式是使用 createMockContext 函数,它会创建一个已经装配好 MockServiceContext,你可以通过 ctx.mock 访问这个服务:

import { createMockContext } from '@fraqjs/plugin-mock';

const ctx = createMockContext();
ctx.install(EchoPlugin);
await ctx.start();

// 通过 ctx.mock 驱动测试
await ctx.mock.receiveFriend({ userId: 10001 }, inmsg`ping`);

createMockContext 默认使用一个直接打印到命令行、没有任何颜色样式的 LogHandler,你也可以通过 logHandler 选项传入其他的日志处理器(例如 @fraqjs/color-log 提供的处理器),方便插件的调试。它还接受 MockService 的全部配置项(如 selfIdbaseTime 等)。

在已有 Context 上安装

如果你已经有一个 Context,也可以直接安装 MockPlugin。它会拦截该 Context 的所有 API 调用并注入事件,你可以通过 ctx.resolve(MockService) 取回服务实例:

import { MockPlugin, MockService } from '@fraqjs/plugin-mock';

ctx.install(MockPlugin);
ctx.install(PluginUnderTest);
await ctx.start();

const mock = ctx.resolve(MockService);

请务必在被测插件之前安装 MockPlugin,以确保 API 拦截在被测插件运行前就位。其他插件也可以通过 inject: { mock: MockService } 来依赖它。

模拟消息事件

MockService 提供了一些函数来模拟不同类型的消息事件:

await ctx.mock.receiveFriend({ userId: 10001 }, inmsg`ping`);
await ctx.mock.receiveGroup({ groupId: 20001, userId: 10001 }, inmsg`/deploy`);
await ctx.mock.receiveTemp({ userId: 10001, groupId: 20001 }, inmsg`hello`);

第一个参数包含了消息事件的相关信息,例如发送者的 QQ 号、所在的群号等,你也可以提供一些其他的信息来覆盖默认值,例如:

await ctx.mock.receiveFriend(
  {
    userId: 10001,
    peerId: 10001,
    senderId: 10001,
    messageSeq: 7,
    time: 123456,
  },
  inmsg`hello`,
);

第二个参数是一个 IncomingSegment[],你可以使用 inmsg 来方便地创建它。inmsg 同样支持字符串、数字、布尔值插值、inseg 插值、自动 trim,用法与之前提到的 msgseg 一致;不同的地方在于 inmsginseg 生成的是 IncomingSegment

生成实体信息

@fraqjs/plugin-mock 还提供了生成好友、群聊以及群成员信息的函数。这些函数将提供的 ID 作为 PCG32seed,并且从内置的词库中选取片段来组合实体信息,因此生成的信息是可复现的。上面提到的模拟消息事件函数会自动调用这些生成函数来生成相关的实体信息,你也可以直接调用它们来获取这些信息:

createRandomFriend(10001);
createRandomGroup(20001);
createRandomGroupMember(20001, 10001);

你也可以提供一些覆盖默认值的选项:

createRandomFriend(10001, { remark: 'Override' });
createRandomGroup(20001, { group_name: 'Override' });

处理 API 调用

MockService 默认会拦截所有出站 API 调用。对于与实体、消息读取有关的 API,它会从内置的 MockInbox 中生成合理的响应;对于其他 API(例如各类发送消息的 API),它默认返回一个空对象 {},这足以让只解构响应中个别字段(如 message_seq)的调用方正常工作。

如果你需要为某个 API 端点设置自定义的响应,请使用 Context 提供的 hookApi 方法。这里有两种用法:

替换响应:不调用 next,直接返回一个响应。此时该 API 调用会被这个 hook 完全接管,不会再流向 MockService,因此也不会被记录到 apiCalls 中。

ctx.hookApi('get_friend_info', (params) => ({
  friend: {
    user_id: params.user_id,
    nickname: 'Override',
    sex: 'unknown',
    qid: 'qid_override',
    remark: '',
    category: {
      category_id: 1,
      category_name: 'General',
    },
  },
}));

观察或改写响应:调用 next 让请求继续流向 MockService,再对其返回值进行处理。此时该调用仍会被 MockService 记录。

ctx.hookApi('send_private_message', async (params, next) => {
  const result = await next();
  return { ...result, message_seq: 42 };
});

内置响应

MockService 会自动为下面这些 API 端点提供响应,无需额外设置。

首先是一些与消息有关的 API,它们的响应会根据提供的信息从已经发送过的消息中生成:

  • get_message
  • get_history_messages
  • mark_message_as_read

除此之外,还有一些与实体信息有关的 API。它们会先寻找之前模拟消息事件中包含的实体信息,如果未发现匹配的实体信息,则会调用前面提到的生成实体信息的函数来生成一个新的实体信息作为响应:

  • get_friend_info
  • get_group_info
  • get_group_member_info

如果要覆盖这些 API 的默认响应,直接使用上面介绍的 hookApi 方法即可。

模拟其他事件

MockService 还提供了一个 emitEvent 方法来模拟其他类型的事件,例如:

await ctx.mock.emitEvent({
  event_type: 'group_join_request',
  time: 123456,
  self_id: ctx.mock.inbox.selfId,
  data: {
    group_id: 20001,
    notification_seq: 7,
    is_filtered: false,
    initiator_id: 10001,
    comment: `
问题:从哪了解到本群的
答案:GitHub
    `.trim(),
  },
});

完整示例

所有刚才的介绍都只涉及了我们该如何模拟用户输入,但更重要的部分在于检查插件对这些输入的响应。MockService 提供了一个名叫 apiCalls 的属性,来记录由 Context 发起的所有 API 调用。每当插件调用一个 API 时,apiCalls 中就会记录下这个调用的端点和参数,你可以检查 apiCalls 的最后若干条记录来验证插件是否正确地调用了预期的 API,以及调用时使用了正确的参数。

例如,仍然以我们的 Echo 机器人为例,让我们编写一个完整的测试:

import assert from 'node:assert/strict';
import test from 'node:test';

import { createMockContext, inmsg } from '@fraqjs/plugin-mock';

test('echo plugin echoes string input prefixed with echo', async () => {
  const ctx = createMockContext();
  ctx.install(EchoPlugin);
  await ctx.start();

  await ctx.mock.receiveFriend({ userId: 10001 }, inmsg`echo Hello`);

  assert.deepEqual(ctx.mock.apiCalls.at(-1), {
    endpoint: 'send_private_message',
    params: {
      user_id: 10001,
      message: [{ type: 'text', data: { text: 'Hello' } }],
    },
  });
});

On this page