Version: 2.0.0-beta.5

单元测试

单元测试

在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。

NoneBot2 使用 Pytest 单元测试框架搭配 NoneBug 插件进行单元测试,通过直接与事件响应器/适配器等交互简化测试流程,更易于编写。

安装 NoneBug

安装 NoneBug 时,Pytest 会作为依赖被一起安装。

要运行 NoneBug,还需要额外安装 Pytest 异步插件 pytest-asyncioanyio,文档将以 pytest-asyncio 为例。

poetry add nonebug pytest-asyncio --dev
# 也可以通过 pip 安装
pip install nonebug pytest-asyncio
提示

建议首先阅读 Pytest 文档 理解相关术语。

加载插件

我们可以使用 Pytest Fixtures 来加载插件,下面是一个示例:

conftest.py
from pathlib import Path
from typing import TYPE_CHECKING, Set

import pytest

if TYPE_CHECKING:
from nonebot.plugin import Plugin


@pytest.fixture
def load_plugins(nonebug_init: None) -> Set["Plugin"]:
import nonebot # 这里的导入必须在函数内

# 加载插件
return nonebot.load_plugins("awesome_bot/plugins")

此 Fixture 的 nonebug_init 形参也是一个 Fixture,用于初始化 NoneBug。

Fixture 名称 load_plugins 可以修改为其他名称,文档以 load_plugins 为例。需要加载插件时,在测试函数添加形参 load_plugins 即可。加载完成后即可使用 import 导入事件响应器。

测试流程

Pytest 会在函数开始前通过 Fixture app(nonebug_app) 初始化 NoneBug 并返回 App 对象。

警告

所有从 nonebot 导入模块的函数都需要首先初始化 NoneBug App,否则会发生不可预料的问题。

在每个测试函数结束时,NoneBug 会自动销毁所有与 NoneBot 相关的资源。所有与 NoneBot 相关的 import 应在函数内进行导入。

随后使用 test_matcher 等测试方法获取到 Context 上下文,通过上下文管理提供的方法(如 should_call_send 等)预定义行为。

在上下文管理器关闭时,Context 会调用 run_test 方法按照预定义行为对事件响应器进行断言(如:断言事件响应和 API 调用等)。

测试样例

提示

将从 utils 导入的 make_fake_messagemake_fake_event 替换为对应平台的消息/事件类型。

load_example 替换为加载插件的 Fixture 名称。

使用 NoneBug 的 test_matcher 可以模拟出一个事件流程。如下是一个简单的示例:

test_weather.py
import pytest
from nonebug import App


@pytest.mark.asyncio
async def test_weather(app: App, load_example):
from examples.weather import weather
from utils import make_fake_event, make_fake_message

# 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型
Message = make_fake_message()

async with app.test_matcher(weather) as ctx:
bot = ctx.create_bot()

msg = Message("/天气 上海")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
event = make_fake_event(_message=msg, _to_me=True)()

ctx.receive_event(bot, event)
ctx.should_call_send(event, "上海的天气是...", True)
ctx.should_finished()

async with app.test_matcher(weather) as ctx:
bot = ctx.create_bot()

msg = Message("/天气 南京")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
event = make_fake_event(_message=msg, _to_me=True)()

ctx.receive_event(bot, event)
ctx.should_call_send(
event,
Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("南京"),
True,
)
ctx.should_rejected()

msg = Message("北京")
event = make_fake_event(_message=msg)()

ctx.receive_event(bot, event)
ctx.should_call_send(event, "北京的天气是...", True)
ctx.should_finished()

async with app.test_matcher(weather) as ctx:
bot = ctx.create_bot()

msg = Message("/天气")
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
event = make_fake_event(_message=msg, _to_me=True)()

ctx.receive_event(bot, event)
ctx.should_call_send(event, "你想查询哪个城市的天气呢?", True)

msg = Message("杭州")
event = make_fake_event(_message=msg)()

ctx.receive_event(bot, event)
ctx.should_call_send(
event,
Message.template("你想查询的城市 {} 暂不支持,请重新输入!").format("杭州"),
True,
)
ctx.should_rejected()

msg = Message("北京")
event = make_fake_event(_message=msg)()

ctx.receive_event(bot, event)
ctx.should_call_send(event, "北京的天气是...", True)
ctx.should_finished()
示例插件
examples/weather.py
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.matcher import Matcher
from nonebot.adapters import Message
from nonebot.params import Arg, CommandArg, ArgPlainText

weather = on_command("weather", rule=to_me(), aliases={"天气", "天气预报"}, priority=5)


@weather.handle()
async def handle_first_receive(matcher: Matcher, args: Message = CommandArg()):
plain_text = args.extract_plain_text() # 首次发送命令时跟随的参数,例:/天气 上海,则args为上海
if plain_text:
matcher.set_arg("city", args) # 如果用户发送了参数则直接赋值


@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(city: Message = Arg(), city_name: str = ArgPlainText("city")):
if city_name not in ["北京", "上海"]: # 如果参数不符合要求,则提示用户重新输入
# 可以使用平台的 Message 类直接构造模板消息
await weather.reject(city.template("你想查询的城市 {city} 暂不支持,请重新输入!"))

city_weather = await get_weather(city_name)
await weather.finish(city_weather)


# 在这里编写获取天气信息的函数
async def get_weather(city: str) -> str:
return f"{city}的天气是..."

在测试用例编写完成后 ,可以使用下面的命令运行单元测试。

pytest test_weather.py