npm i jest koishi-test-utils -D
# 或者
yarn add jest koishi-test-utils -D
# 单元测试
如果你是一位插件开发者,比起让机器人真正运行起来,你或许会更希望使用单元测试,因为它具有许多前者所不具有的优点:
- 可以在无网络的情况下运行
- 可以模拟出多用户交互等复杂情况
- 可以在内存中模拟你想要的数据库
- 能够有效避免腾讯风控代理的损失
- 便于调试与错误定位
本章将介绍官方库 koishi-test-utils
。它一个基于 Jest 的单元测试工具集,你可以用它来快速检验你编写的 Koishi 插件和数据库实现。
# 准备工作
安装最新版本的 Jest 和 koishi-test-utils:
提示
你可以在 这里 看到完整的接口列表和所需的最低版本。
# 使用 TypeScript
如果你使用 TypeScript 进行开发,你可能还需要下面这些依赖(当然你可能已经安装了它们):
npm i typescript @types/node @types/jest ts-jest -D
# 或者
yarn add typescript @types/node @types/jest ts-jest -D
接着编辑你的 jest.config.js
文件:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js', 'json'],
}
# 模拟事件上报
当你不希望真正用服务器却希望测试 App 对某个事件上报的响应时,你可以使用 MockedApp 来模拟一次事件上报:
const { MockedApp } = require('koishi-test-utils')
// 这里的 MockedApp 是 App 的一个子类,因此你仍然可以像过去那样编写代码
const app = new MockedApp()
// 这是一个简单的中间件例子,下面将测试这个中间件
app.middleware(({ message, $send }, next) => {
if (message === '天王盖地虎') return $send('宝塔镇河妖')
return next()
})
test('example', async () => {
// 尝试接收一个 message 事件
await app.receiveMessage('user', '天王盖地虎', 123)
// 判断 app 应该最终发送了这个请求
// 这里的请求名相当于 sender 中对应的接口名,不用写 async
// 请求参数与 sender 相一致
app.shouldHaveLastRequest('send_private_msg', {
userId: 123,
message: '宝塔镇河妖',
})
// 再次尝试接收一个 message 事件
await app.receiveMessage('user', '宫廷玉液酒', 123)
// 判断 app 应该最终没有发送任何请求
app.shouldHaveNoRequests()
})
# 模拟会话
Koishi 本身不需要会话的概念,因为 Meta 对象本身就具有 快捷操作 功能。但是在单元测试中,我们可能经常需要让一个用户多次向机器人发送信息,这时一个会话系统就变得非常有用了。
const { MockedApp } = require('koishi-test-utils')
// 创建一个无服务端的 App 实例
const app = new MockedApp()
// 创建一个 QQ 号为 123 的私聊会话
const session = app.createSession('user', 123)
// 还是刚刚那个例子
app.middleware(({ message, $send }, next) => {
if (message === '天王盖地虎') return $send('宝塔镇河妖')
return next()
})
test('example', () => {
// 将 foo 发送给机器人将会获得 bar 的回复
await session.shouldHaveResponse('天王盖地虎', '宝塔镇河妖')
// 将 foo 发送给机器人将会获得某些回复
await session.shouldHaveResponse('天王盖地虎')
// 将 foo 发送给机器人后将会获得与快照一致的回复
await session.shouldMatchSnapshot('天王盖地虎')
// 将 foo 发送给机器人将不会获得任何回复
await session.shouldHaveNoResponse('宫廷玉液酒')
})
# 模拟服务器
如果你想使用真正的 HTTP / WebSocket 服务器来测试,Koishi 也提供了相应的办法:
const { createHttpServer } = require('koishi-test-utils')
test('example', async () => {
// 创建一个真正的 CQHTTP 服务端
const server = await createHttpServer(token)
// 创建一个与之关联的 App
const app = server.createBoundApp()
// 运行 Koishi 应用
await app.start()
// 服务端向 Koishi 上报事件
await server.post(meta)
// 设置客户端请求的结果
server.setResponse(method, data)
// 判断上一次请求的内容
server.shouldHaveLastRequest(method, params)
// 关闭服务端和所有关联的 App
await server.close()
})
# 模拟工具函数
如果我们编写的插件中含有延时或者随机效果,测试工作会变得困难不少——因为我们希望所有测试的行为都能在短时间内符合预期地完成。这个时候,koishi-test-utils 也提供了一种解决方式。
// 这里的 utils 相当于 koishi-utils 的一个副本
// 在此基础上额外提供了测试时进行控制的方法
const { utils } = require('koishi-test-utils')
// 此后插件中的 randomPick() 调用将永远返回数组的第一个元素
utils.randomPick.mockIndex(0)
// 此后插件中的 randomInt() 调用将永远返回 3
utils.randomInt.mockReturnValue(3)
// 此后插件中的 sleep() 调用将不等待直接返回 Promise.resolve()
utils.sleep.mockResolvedValue()
这些函数也有对应的 once 版本,它们将只生效一次:
// 下一次插件中的 randomPick() 调用将返回数组的第一个元素
utils.randomPick.mockIndexOnce(0)
// 下一次插件中的 randomInt() 调用将返回 3
utils.randomInt.mockReturnValueOnce(3)
// 下一次插件中的 sleep() 调用将不等待直接返回 Promise.resolve()
utils.sleep.mockResolvedValueOnce()
# 模拟数据库
koishi-database-memory 是 Koishi 的一个数据库实现,只不过它将所有数据都留在内存中,方便调试:
const { App } = require('koishi-core')
require('koishi-database-memory')
// 使用内存数据库
const app = new App({
database: { memory: {} },
})
当然你也可以和上面的 MockedApp 结合起来使用:
const { MockedApp } = require('koishi-test-utils')
require('koishi-database-memory')
// 使用内存数据库
const app = new MockedApp({
database: { memory: {} },
})
# 测试数据库
你可以使用 testDatabase()
方法测试你编写的数据库。下面是一个简单的例子,它测试了我们刚刚介绍的内存数据库:
const { testDatabase } = require('koishi-test-utils')
require('koishi-database-memory')
testDatabase({ memory: {} })
// 传入两个参数
// 第一个参数是 App 的构造选项中的 database 参数
// 第二个参数是每个测试阶段要执行的钩子函数,这些函数将用于手动清理数据库
testDatabase({ memory: {} }, {
beforeEachUser: app => app.database.memory.store.user = [],
beforeEachGroup: app => app.database.memory.store.group = [],
})
这个函数将测试所有 内置的数据库方法 是否已经被实现。
除此以外,你还可以测试你扩展的数据库接口,就像这样:
const { testDatabase } = require('koishi-test-utils')
// 返回一个 App 实例
const app = testDatabase(options, hooks)
test('other methods', () => {
// do something with `app`
})
← 输出与日志