📝 Docs: 添加 nonebug 单元测试文档 (#929)

Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: StarHeart <starheart233@gmail.com>
This commit is contained in:
MingxuanGame 2022-04-30 16:04:41 +08:00 committed by GitHub
parent 34186830ab
commit 11b6e1ba98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 554 additions and 12 deletions

View File

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

View File

@ -1,10 +0,0 @@
---
description: 编写单元测试
options:
menu:
weight: 80
category: advanced
---
# 单元测试

View File

@ -0,0 +1,106 @@
---
sidebar_position: 1
description: 使用 NoneBug 测试机器人
slug: /advanced/unittest/
options:
menu:
weight: 80
category: advanced
---
import CodeBlock from "@theme/CodeBlock";
# 单元测试
[单元测试](https://zh.wikipedia.org/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95)
> 在计算机编程中单元测试Unit Testing又称为模块测试是针对程序模块软件设计的最小单位来进行正确性检验的测试工作。
NoneBot2 使用 [Pytest](https://docs.pytest.org) 单元测试框架搭配 [NoneBug](https://github.com/nonebot/nonebug) 插件进行单元测试,通过直接与事件响应器/适配器等交互简化测试流程,更易于编写。
## 安装 NoneBug
安装 NoneBug 时Pytest 会作为依赖被一起安装。
要运行 NoneBug还需要额外安装 Pytest 异步插件 `pytest-asyncio` 或 `anyio`,文档将以 `pytest-asyncio` 为例。
```bash
poetry add nonebug pytest-asyncio --dev
# 也可以通过 pip 安装
pip install nonebug pytest-asyncio
```
:::tip 提示
建议首先阅读 [Pytest 文档](https://docs.pytest.org) 理解相关术语。
:::
## 加载插件
我们可以使用 Pytest **Fixtures** 来加载插件,下面是一个示例:
```python title=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`](https://github.com/nonebot/nonebug/blob/master/nonebug/fixture.py) 形参也是一个 Fixture用于初始化 NoneBug。
Fixture 名称 `load_plugins` 可以修改为其他名称,文档以 `load_plugins` 为例。需要加载插件时,在测试函数添加形参 `load_plugins` 即可。加载完成后即可使用 `import` 导入事件响应器。
## 测试流程
Pytest 会在函数开始前通过 Fixture `app`(nonebug_app) **初始化 NoneBug** 并返回 `App` 对象。
:::warning 警告
所有从 `nonebot` 导入模块的函数都需要首先初始化 NoneBug App否则会发生不可预料的问题。
在每个测试函数结束时NoneBug 会自动销毁所有与 NoneBot 相关的资源。所有与 NoneBot 相关的 import 应在函数内进行导入。
:::
随后使用 `test_matcher` 等测试方法获取到 `Context` 上下文,通过上下文管理提供的方法(如 `should_call_send` 等)预定义行为。
在上下文管理器关闭时,`Context` 会调用 `run_test` 方法按照预定义行为对事件响应器进行断言(如:断言事件响应和 API 调用等)。
## 测试样例
:::tip 提示
将从 `utils` 导入的 `make_fake_message``make_fake_event` 替换为对应平台的消息/事件类型。
将 `load_example` 替换为加载插件的 Fixture 名称。
:::
使用 NoneBug 的 `test_matcher` 可以模拟出一个事件流程。如下是一个简单的示例:
import WeatherSource from "!!raw-loader!@site/../tests/examples/weather.py";
import WeatherTest from "!!raw-loader!@site/../tests/test_examples/test_weather.py";
<CodeBlock className="language-python" title="test_weather.py">
{WeatherTest}
</CodeBlock>
<details>
<summary>示例插件</summary>
<CodeBlock className="language-python" title="examples/weather.py">
{WeatherSource}
</CodeBlock>
</details>
在测试用例编写完成后 ,可以使用下面的命令运行单元测试。
```bash
pytest test_weather.py
```

View File

@ -0,0 +1,3 @@
{
"label": "单元测试"
}

View File

@ -0,0 +1,162 @@
---
sidebar_position: 4
description: 测试适配器
---
# 测试适配器
通常来说,测试适配器需要测试这三项。
1. 测试连接
2. 测试事件转化
3. 测试 API 调用
## 注册适配器
任何的适配器都需要注册才能起作用。
我们可以使用 Pytest 的 Fixtures在测试开始前初始化 NoneBot 并**注册适配器**。
我们假设适配器为 `nonebot.adapters.test`
```python {20,21} title=conftest.py
from pathlib import Path
import pytest
from nonebug import App
# 如果适配器采用 nonebot.adapters monospace 则需要使用此hook方法正确添加路径
@pytest.fixture
def import_hook():
import nonebot.adapters
nonebot.adapters.__path__.append( # type: ignore
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
)
@pytest.fixture
async def init_adapter(app: App, import_hook):
import nonebot
from nonebot.adapters.test import Adapter
driver = nonebot.get_driver()
driver.register_adapter(Adapter)
```
## 测试连接
任何的适配器的连接方式主要有以下 4 种:
1. 反向 HTTPWebHook
2. 反向 WebSocket
3. ~~正向 HTTP尚未实现~~
4. ~~正向 WebSocket尚未实现~~
NoneBug 的 `test_server` 方法可以供我们测试反向连接方式。
`test_server``get_client` 方法可以获取 HTTP/WebSocket 客户端。
我们假设适配器 HTTP 上报地址为 `/test/http`,反向 WebSocket 地址为 `/test/ws`,上报机器人 ID
使用请求头 `Bot-ID` 来演示如何通过 NoneBug 测试适配器。
```python {8,16,17,19-22,26,34,36-39} title=test_connection.py
from pathlib import Path
import pytest
from nonebug import App
@pytest.mark.asyncio
@pytest.mark.parametrize(
"endpoints", ["/test/http"]
)
async def test_http(app: App, init_adapter, endpoints: str):
import nonebot
async with app.test_server() as ctx:
client = ctx.get_client()
body = {"post_type": "test"}
headers = {"Bot-ID": "test"}
resp = await client.post(endpoints, json=body, headers=headers)
assert resp.status_code == 204 # 检测状态码是否正确
bots = nonebot.get_bots()
assert "test" in bots # 检测是否连接成功
@pytest.mark.asyncio
@pytest.mark.parametrize(
"endpoints", ["/test/ws"]
)
async def test_ws(app: App, init_adapter, endpoints: str):
import nonebot
async with app.test_server() as ctx:
client = ctx.get_client()
headers = {"Bot-ID": "test"}
async with client.websocket_connect(endpoints, headers=headers) as ws:
bots = nonebot.get_bots()
assert "test" in bots # 检测是否连接成功
```
## 测试事件转化
事件转化就是将原始数据反序列化为 `Event` 对象的过程。
测试事件转化就是测试反序列化是否按照预期转化为对应 `Event` 类型。
下面将以 `dict_to_event` 作为反序列化方法,`type` 为 `test` 的事件类型为 `TestEvent` 来演示如何测试事件转化。
```python {8,9} title=test_event.py
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_event(app: App, init_adapter):
from nonebot.adapters.test import Adapter, TestEvent
event = Adapter.dict_to_event({"post_type": "test"}) # 反序列化原始数据
assert isinstance(event, TestEvent) # 断言类型是否与预期一致
```
## 测试 API 调用
将消息序列化为原始数据并由适配器发送到协议端叫做 API 调用。
测试 API 调用就是调用 API 并验证返回与预期返回是否一致。
```python {16-18,23-32} title=test_call_api.py
import asyncio
from pathlib import Path
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_ws(app: App, init_adapter):
import nonebot
async with app.test_server() as ctx:
client = ctx.get_client()
headers = {"Bot-ID": "test"}
async def call_api():
bot = nonebot.get_bot("test")
return await bot.test_api()
async with client.websocket_connect("/test/ws", headers=headers) as ws:
task = asyncio.create_task(call_api())
# received = await ws.receive_text()
# received = await ws.receive_bytes()
received = await ws.receive_json()
assert received == {"action": "test_api"} # 检测调用是否与预期一致
response = {"result": "test"}
# await ws.send_text(...)
# await ws.send_bytes(...)
await ws.send_json(response, mode="bytes")
result = await task
assert result == response # 检测返回是否与预期一致
```

View File

@ -0,0 +1,122 @@
---
sidebar_position: 3
description: 测试事件响应处理
---
# 测试事件响应处理行为
除了 `send`,事件响应器还有其他的操作,我们也需要对它们进行测试,下面我们将定义如下事件响应器操作的预期行为对对应的事件响应器操作进行测试。
## should_finished
定义事件响应器预期结束当前事件的整个处理流程。
适用事件响应器操作:[`finish`](../../tutorial/plugin/matcher-operation.md#finish)。
<!-- markdownlint-disable MD033 -->
<details>
<summary>示例插件</summary>
```python title=example.py
from nonebot import on_message
foo = on_message()
@foo.handle()
async def _():
await foo.finish("test")
```
</details>
```python {13}
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugins):
from awesome_bot.plugins.example import foo
async with app.test_matcher(foo) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True)
ctx.should_finished()
```
## should_paused
定义事件响应器预期立即结束当前事件处理依赖并等待接收一个新的事件后进入下一个事件处理依赖。
适用事件响应器操作:[`pause`](../../tutorial/plugin/matcher-operation.md#pause)。
<details>
<summary>示例插件</summary>
```python title=example.py
from nonebot import on_message
foo = on_message()
@foo.handle()
async def _():
await foo.pause("test")
```
</details>
```python {13}
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugins):
from awesome_bot.plugins.example import foo
async with app.test_matcher(foo) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True)
ctx.should_paused()
```
## should_rejected
定义事件响应器预期立即结束当前事件处理依赖并等待接收一个新的事件后再次执行当前事件处理依赖。
适用事件响应器操作:[`reject`](../../tutorial/plugin/matcher-operation.md#reject)
、[`reject_arg`](../../tutorial/plugin/matcher-operation.md#reject_arg)
和 [`reject_receive`](../../tutorial/plugin/matcher-operation.md#reject_receive)。
<details>
<summary>示例插件</summary>
```python title=example.py
from nonebot import on_message
foo = on_message()
@foo.got("key")
async def _():
await foo.reject("test")
```
</details>
```python {13}
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugins):
from awesome_bot.plugins.example import foo
async with app.test_matcher(foo) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True)
ctx.should_rejected()
```

View File

@ -0,0 +1,159 @@
---
sidebar_position: 2
description: 测试事件响应和 API 调用
---
# 测试事件响应和 API 调用
事件响应器通过 `Rule``Permission` 来判断当前事件是否触发事件响应器,通过 `send` 发送消息或使用 `call_api` 调用平台 API这里我们将对上述行为进行测试。
## 定义预期响应行为
NoneBug 提供了六种定义 `Rule``Permission` 的预期行为的方法来进行测试:
- `should_pass_rule`
- `should_not_pass_rule`
- `should_ignore_rule`
- `should_pass_permission`
- `should_not_pass_permission`
- `should_ignore_permission`
以下为示例代码
<!-- markdownlint-disable MD033 -->
<details>
<summary>示例插件</summary>
```python title=example.py
from nonebot import on_message
async def always_pass():
return True
async def never_pass():
return False
foo = on_message(always_pass)
bar = on_message(never_pass, permission=never_pass)
```
</details>
```python {12,13,19,20,27,28}
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugins):
from awesome_bot.plugins.example import foo, bar
async with app.test_matcher(foo) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_pass_rule()
ctx.should_pass_permission()
async with app.test_matcher(bar) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_not_pass_rule()
ctx.should_not_pass_permission()
# 如需忽略规则/权限不通过
async with app.test_matcher(bar) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_ignore_rule()
ctx.should_ignore_permission()
```
## 定义预期 API 调用行为
在[事件响应器操作](../../tutorial/plugin/matcher-operation.md)和[调用平台 API](../../tutorial/call-api.md) 中,我们已经了解如何向发送消息或调用平台 `API`。接下来对 [`send`](../../tutorial/plugin/matcher-operation.md#send) 和 [`call_api`](../../api/adapters/index.md#Bot-call_api) 进行测试。
### should_call_send
定义事件响应器预期发送消息,包括使用 [`send`](../../tutorial/plugin/matcher-operation.md#send)、[`finish`](../../tutorial/plugin/matcher-operation.md#finish)、[`pause`](../../tutorial/plugin/matcher-operation.md#pause)、[`reject`](../../tutorial/plugin/matcher-operation.md#reject) 以及 [`got`](../../tutorial/plugin/create-handler.md#使用-got-装饰器) 的 prompt 等方法发送的消息。
`should_call_send` 需要提供四个参数:
- `event`:事件对象。
- `message`:预期的消息对象,可以是`str`、[`Message`](../../api/adapters/index.md#Message) 或 [`MessageSegment`](../../api/adapters/index.md#MessageSegment)。
- `result``send` 的返回值,将会返回给插件。
- `**kwargs``send` 方法的额外参数。
<details>
<summary>示例插件</summary>
```python title=example.py
from nonebot import on_message
foo = on_message()
@foo.handle()
async def _():
await foo.send("test")
```
</details>
```python {12}
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugins):
from awesome_bot.plugins.example import foo
async with app.test_matcher(foo) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_call_send(event, "test", True)
```
### should_call_api
定义事件响应器预期调用机器人 API 接口,包括使用 `call_api` 或者直接使用 `bot.some_api` 的方式调用 API。
`should_call_api` 需要提供四个参数:
- `api`API 名称。
- `data`:预期的请求数据。
- `result``call_api` 的返回值,将会返回给插件。
- `**kwargs``call_api` 方法的额外参数。
<details>
<summary>示例插件</summary>
```python
from nonebot import on_message
from nonebot.adapters import Bot
foo = on_message()
@foo.handle()
async def _(bot: Bot):
await bot.example_api(test="test")
```
</details>
```python {12}
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_matcher(app: App, load_plugins):
from awesome_bot.plugins.example import foo
async with app.test_matcher(foo) as ctx:
bot = ctx.create_bot()
event = make_fake_event()() # 此处替换为平台事件
ctx.receive_event(bot, event)
ctx.should_call_api("example_api", {"test": "test"}, True)
```

View File

@ -84,7 +84,7 @@ async def handle_func():
```python {3-5}
matcher = on_message()
@matcher.got("key")
@matcher.got("key", prompt="Key?")
async def handle_func(key: Message = Arg()):
# do something here
```