mirror of
				https://github.com/nonebot/nonebot2.git
				synced 2025-10-31 06:56:39 +00:00 
			
		
		
		
	🔖 Release 2.3.1
This commit is contained in:
		| @@ -0,0 +1,212 @@ | ||||
| --- | ||||
| sidebar_position: 1 | ||||
| description: 使用 NoneBug 进行单元测试 | ||||
|  | ||||
| slug: /best-practice/testing/ | ||||
| --- | ||||
|  | ||||
| # 配置与测试事件响应器 | ||||
|  | ||||
| import Tabs from "@theme/Tabs"; | ||||
| import TabItem from "@theme/TabItem"; | ||||
|  | ||||
| > 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 | ||||
|  | ||||
| 为了保证代码的正确运行,我们不仅需要对错误进行跟踪,还需要对代码进行正确性检验,也就是测试。NoneBot 提供了一个测试工具——NoneBug,它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件,可以帮助我们便捷地进行单元测试。 | ||||
|  | ||||
| :::tip 提示 | ||||
| 建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。 | ||||
| ::: | ||||
|  | ||||
| ## 安装 NoneBug | ||||
|  | ||||
| 在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug: | ||||
|  | ||||
| <Tabs groupId="tool"> | ||||
|   <TabItem value="poetry" label="Poetry" default> | ||||
|  | ||||
| ```bash | ||||
| poetry add nonebug -G test | ||||
| ``` | ||||
|  | ||||
|   </TabItem> | ||||
|   <TabItem value="pdm" label="PDM"> | ||||
|  | ||||
| ```bash | ||||
| pdm add nonebug -dG test | ||||
| ``` | ||||
|  | ||||
|   </TabItem> | ||||
|   <TabItem value="pip" label="pip"> | ||||
|  | ||||
| ```bash | ||||
| pip install nonebug | ||||
| ``` | ||||
|  | ||||
|   </TabItem> | ||||
| </Tabs> | ||||
|  | ||||
| 要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例: | ||||
|  | ||||
| <Tabs groupId="tool"> | ||||
|   <TabItem value="poetry" label="Poetry" default> | ||||
|  | ||||
| ```bash | ||||
| poetry add pytest-asyncio -G test | ||||
| ``` | ||||
|  | ||||
|   </TabItem> | ||||
|   <TabItem value="pdm" label="PDM"> | ||||
|  | ||||
| ```bash | ||||
| pdm add pytest-asyncio -dG test | ||||
| ``` | ||||
|  | ||||
|   </TabItem> | ||||
|   <TabItem value="pip" label="pip"> | ||||
|  | ||||
| ```bash | ||||
| pip install pytest-asyncio | ||||
| ``` | ||||
|  | ||||
|   </TabItem> | ||||
| </Tabs> | ||||
|  | ||||
| ## 配置测试 | ||||
|  | ||||
| 在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容: | ||||
|  | ||||
| ```python title=tests/conftest.py | ||||
| import pytest | ||||
| import nonebot | ||||
| # 导入适配器 | ||||
| from nonebot.adapters.console import Adapter as ConsoleAdapter | ||||
|  | ||||
| @pytest.fixture(scope="session", autouse=True) | ||||
| def load_bot(): | ||||
|     # 加载适配器 | ||||
|     driver = nonebot.get_driver() | ||||
|     driver.register_adapter(ConsoleAdapter) | ||||
|  | ||||
|     # 加载插件 | ||||
|     nonebot.load_from_toml("pyproject.toml") | ||||
| ``` | ||||
|  | ||||
| 这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置: | ||||
|  | ||||
| ```python {3,5,7-9} title=tests/conftest.py | ||||
| import os | ||||
|  | ||||
| from nonebug import NONEBOT_INIT_KWARGS | ||||
|  | ||||
| os.environ["ENVIRONMENT"] = "test" | ||||
|  | ||||
| def pytest_configure(config: pytest.Config): | ||||
|     config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")} | ||||
| ``` | ||||
|  | ||||
| ## 编写插件测试 | ||||
|  | ||||
| 在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块: | ||||
|  | ||||
| <details> | ||||
|   <summary>插件示例</summary> | ||||
|  | ||||
| ```python title=weather/__init__.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 CommandArg, ArgPlainText | ||||
|  | ||||
| weather = on_command("天气", rule=to_me(), aliases={"weather", "天气预报"}) | ||||
|  | ||||
| @weather.handle() | ||||
| async def handle_function(matcher: Matcher, args: Message = CommandArg()): | ||||
|     if args.extract_plain_text(): | ||||
|         matcher.set_arg("location", args) | ||||
|  | ||||
| @weather.got("location", prompt="请输入地名") | ||||
| async def got_location(location: str = ArgPlainText()): | ||||
|     if location not in ["北京", "上海", "广州", "深圳"]: | ||||
|         await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") | ||||
|     await weather.finish(f"今天{location}的天气是...") | ||||
| ``` | ||||
|  | ||||
| </details> | ||||
|  | ||||
| ```python {4,5,9,11-16} title=tests/test_weather.py | ||||
| from datetime import datetime | ||||
|  | ||||
| import pytest | ||||
| from nonebug import App | ||||
| from nonebot.adapters.console import User, Message, MessageEvent | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_weather(app: App): | ||||
|     from awesome_bot.plugins.weather import weather | ||||
|  | ||||
|     event = MessageEvent( | ||||
|         time=datetime.now(), | ||||
|         self_id="test", | ||||
|         message=Message("/天气 北京"), | ||||
|         user=User(user_id=123456789), | ||||
|     ) | ||||
| ``` | ||||
|  | ||||
| 在上面的代码中,我们引入了 NoneBug 的测试 `App` 对象,以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中,我们导入了要进行测试的事件响应器 `weather`。请注意,由于需要等待 NoneBot 初始化并加载插件完毕,插件内容必须在**测试函数内部**进行导入。然后,我们创建了一个 `MessageEvent` 事件对象,它模拟了一个用户发送了 `/天气 北京` 的消息。接下来,我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器: | ||||
|  | ||||
| ```python {11-15} title=tests/test_weather.py | ||||
| @pytest.mark.asyncio | ||||
| async def test_weather(app: App): | ||||
|     from awesome_bot.plugins.weather import weather | ||||
|  | ||||
|     event = MessageEvent( | ||||
|         time=datetime.now(), | ||||
|         self_id="test", | ||||
|         message=Message("/天气 北京"), | ||||
|         user=User(user_id=123456789), | ||||
|     ) | ||||
|     async with app.test_matcher(weather) as ctx: | ||||
|         bot = ctx.create_bot() | ||||
|         ctx.receive_event(bot, event) | ||||
|         ctx.should_call_send(event, "今天北京的天气是...", result=None) | ||||
|         ctx.should_finished(weather) | ||||
| ``` | ||||
|  | ||||
| 这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中,我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例,并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后,我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中,我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息,并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败,测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。 | ||||
|  | ||||
| 为了测试更复杂的情况,我们可以为添加更多的测试用例。例如,我们可以测试用户输入了一个不支持的地名时机器人的反应: | ||||
|  | ||||
| ```python {17-21,23-26} title=tests/test_weather.py | ||||
| def make_event(message: str = "") -> MessageEvent: | ||||
|     return MessageEvent( | ||||
|         time=datetime.now(), | ||||
|         self_id="test", | ||||
|         message=Message(message), | ||||
|         user=User(user_id=123456789), | ||||
|     ) | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_weather(app: App): | ||||
|     from awesome_bot.plugins.weather import weather | ||||
|  | ||||
|     async with app.test_matcher(weather) as ctx: | ||||
|         ...  # 省略前面的测试用例 | ||||
|  | ||||
|     async with app.test_matcher(weather) as ctx: | ||||
|         bot = ctx.create_bot() | ||||
|         event = make_event("/天气 南京") | ||||
|         ctx.receive_event(bot, event) | ||||
|         ctx.should_call_send(event, "你想查询的城市 南京 暂不支持,请重新输入!", result=None) | ||||
|         ctx.should_rejected(weather) | ||||
|  | ||||
|         event = make_event("北京") | ||||
|         ctx.receive_event(bot, event) | ||||
|         ctx.should_call_send(event, "今天北京的天气是...", result=None) | ||||
|         ctx.should_finished(weather) | ||||
| ``` | ||||
|  | ||||
| 在上面的代码中,我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后,我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`,并使用 `ctx.should_finished` 来断言机器人应该结束会话。 | ||||
|  | ||||
| 更多的 NoneBug 用法将在后续章节中介绍。 | ||||
		Reference in New Issue
	
	Block a user