第 26 章:测试与质量

1 | 卷四:纵深 |
411 个文件、13 万行代码,怎么保证改了不会破坏已有功能?QwenPaw 用四层测试架构——单元测试、契约测试、集成测试、端到端测试——加上 CI 门禁和 pre-commit 检查。
四层测试架构
1 | tests/ |
自动标记:unit/ 下的测试自动加 @pytest.mark.unit,integration/ 加 @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_handler、mock_enqueue、event_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
22class 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
3class 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 | [tool.coverage.run] |
30% 的阈值看似不高,但考虑到 411 个文件中大量是渠道适配器和技能(模板化代码),核心模块的覆盖率远高于 30%。
CI 门禁
PR 必须通过的检查:1
2
3
4
5
6
7
8
9
10
11
121. 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
3async 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
5async 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
4class TestMyChannelContract(ChannelContractTest):
def create_instance(self):
return MyChannel(process=mock_process)
# 20 个 test_* 方法自动继承
工程权衡
为什么契约测试比单元测试更重要?
Channel 和 Provider 的实现细节各不相同,但接口必须一致。契约测试保证”接口不变”——只要契约通过,上层代码就不会因为接口变化而崩溃。单元测试验证具体实现,但接口一致性是更大的风险。
为什么没有性能测试?
QwenPaw 是个人助手,不是高并发服务。性能瓶颈通常在 LLM API 的响应时间(秒级),不在 QwenPaw 的代码(毫秒级)。性能测试的 ROI 不高。如果有性能问题,通常通过配置(max_concurrent、max_qpm)而非代码优化解决。
自检
- 知道四层测试架构:unit / contract / integration / e2e
- 知道 conftest.py 提供了 mock_llm_provider、temp_workspace 等 fixture
- 知道契约测试验证 Channel 的 20 个接口合规性检查
- 知道 CI 的三个 HARD gate:pre-commit、tests、channel-contract
卷四”纵深”完成。配置系统、记忆管理、自治任务、多智能体协作、插件系统、命令行部署、测试质量——QwenPaw 的高级功能都覆盖了。全书的核心内容到这里结束。附录提供快速参考。