第 26 章:测试与质量

个人公众号
1
2
卷四:纵深
[20] 配置 -> [21] 记忆 -> [22] 自治 -> [23] 多智能体 -> [24] 插件 -> [25] CLI -> [26] 测试 <- you are here

411 个文件、13 万行代码,怎么保证改了不会破坏已有功能?QwenPaw 用四层测试架构——单元测试、契约测试、集成测试、端到端测试——加上 CI 门禁和 pre-commit 检查。


四层测试架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tests/
unit/ # 单元测试(~60 个文件)
channels/ # 15 个 Channel 测试
providers/ # 10 个 Provider 测试
agents/ # Agent 和工具测试
security/ # 安全模块测试
cli/ # CLI 命令测试
...
contract/ # 契约测试(接口合规性)
channels/ # 11 个 Channel 契约测试
providers/ # Provider 契约测试
integration/ # 集成测试
e2e/ # 端到端测试(占位)
conftest.py # 全局 fixtures

自动标记:unit/ 下的测试自动加 @pytest.mark.unitintegration/@pytest.mark.integration

核心 Fixtures

conftest.py 提供了 10+ 个全局 fixtures:

Fixture用途
temp_workspace隔离的临时工作区(测试后清理)
temp_copaw_home隔离的 HOME 环境(清除敏感环境变量)
mock_llm_provider模拟 LLM(返回固定响应)
mock_channel模拟 Channel
minimal_config最小配置字典
mock_process_handler模拟 ProcessHandler(返回完成事件)
mock_channel_config模拟 Channel 配置
mock_provider_factory模拟 Provider 工厂

Channel 测试有独立的 conftest.py,提供 mock_process_handlermock_enqueueevent_loop 等 fixture。

契约测试——接口的守卫

契约测试验证 Channel 和 Provider 实现了所有必需接口。ChannelContractTest 检查 20 个验证点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ChannelContractTest(BaseContractTest):
# 抽象方法检查
test_no_abstract_methods_remaining
test_required_methods_not_raising_not_implemented

# 方法存在性检查
test_has_channel_type_attribute
test_has_start_method
test_has_stop_method
test_has_send_method
test_has_from_config_method

# 签名兼容性检查
test_start_method_signature_compatible
test_stop_method_signature_compatible

# 属性检查
test_policy_attributes_exist
test_policy_attributes_types

# Session 管理检查
test_resolve_session_id_returns_str

写一个新的 Channel 契约测试只需继承并实现 create_instance()

1
2
3
class TestChatXContract(ChannelContractTest):
def create_instance(self):
return ChatXChannel(process=mock_process, bot_token="test")

Mock 模式

项目只用 unittest.mock(标准库),不用第三方 mock 库:

1
2
3
4
5
6
7
8
9
10
11
# 同步接口用 MagicMock
mock_provider = MagicMock()
mock_provider.chat.return_value = "Mock response"

# 异步接口用 AsyncMock
mock_process = AsyncMock()
mock_process.side_effect = async_generator yielding events

# 替换模块级对象用 patch
with patch("qwenpaw.cli.channels_cmd.load_agent_config"):
result = runner.invoke(channels_group, ["list"])

HTTP mock 用自定义的 MockAiohttpSession——基于期望的 mock,支持 expect_post()expect_get()

覆盖率配置

1
2
3
4
5
6
7
8
9
10
[tool.coverage.run]
source = ["src/qwenpaw"]
fail_under = 30 # 最低 30% 覆盖率

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]

30% 的阈值看似不高,但考虑到 411 个文件中大量是渠道适配器和技能(模板化代码),核心模块的覆盖率远高于 30%。

CI 门禁

PR 必须通过的检查:

1
2
3
4
5
6
7
8
9
10
11
12
1. pre-commit.yml (HARD gate)
black, flake8, mypy, pylint
失败 = 不能合并

2. tests.yml (需审批)
Python 3.10 + 3.13 on Ubuntu
Python 3.10 on macOS + Windows
覆盖率报告

3. channel-tests.yml (改 Channel 时)
契约测试 (HARD gate)
单元测试 (soft gate)

常见测试模式

测试工具函数

1
2
3
async def test_http_request_valid_url():
result = await http_request(url="https://example.com")
assert "Error" not in result.content[0].text

测试 Provider

1
2
3
4
5
async def test_deepseek_connection():
provider = PROVIDER_DEEPSEEK
provider.api_key = "test-key"
ok, msg = await provider.check_connection(timeout=5)
# 用 mock 模拟 API 响应

测试 Channel 契约

1
2
3
4
class TestMyChannelContract(ChannelContractTest):
def create_instance(self):
return MyChannel(process=mock_process)
# 20 个 test_* 方法自动继承

工程权衡

为什么契约测试比单元测试更重要?

Channel 和 Provider 的实现细节各不相同,但接口必须一致。契约测试保证”接口不变”——只要契约通过,上层代码就不会因为接口变化而崩溃。单元测试验证具体实现,但接口一致性是更大的风险。

为什么没有性能测试?

QwenPaw 是个人助手,不是高并发服务。性能瓶颈通常在 LLM API 的响应时间(秒级),不在 QwenPaw 的代码(毫秒级)。性能测试的 ROI 不高。如果有性能问题,通常通过配置(max_concurrentmax_qpm)而非代码优化解决。

自检

  • 知道四层测试架构:unit / contract / integration / e2e
  • 知道 conftest.py 提供了 mock_llm_provider、temp_workspace 等 fixture
  • 知道契约测试验证 Channel 的 20 个接口合规性检查
  • 知道 CI 的三个 HARD gate:pre-commit、tests、channel-contract

卷四”纵深”完成。配置系统、记忆管理、自治任务、多智能体协作、插件系统、命令行部署、测试质量——QwenPaw 的高级功能都覆盖了。全书的核心内容到这里结束。附录提供快速参考。