测试策略

概述

nanobot 使用 pytestpytest-asyncio 进行测试。测试套件侧重于对核心组件进行单元测试,采用轻量级的测试替身(不依赖重型 mock 框架)。所有测试都可以在无需外部服务的情况下快速运行。

测试基础设施

框架

工具

版本

用途

pytest

≥ 9.0

测试运行器和断言

pytest-asyncio

≥ 1.3

异步测试支持

ruff

≥ 0.1

代码检查(非测试工具,但属于 CI 质量保障的一部分)

配置(pyproject.toml

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
  • asyncio_mode = "auto":所有 async 测试函数会自动被识别为异步测试(无需在每个测试上添加 @pytest.mark.asyncio,不过代码库中为了清晰起见仍然显式使用了该装饰器)。

  • testpaths = ["tests"]:测试发现从 tests/ 目录开始。

运行测试

# 运行所有测试
pytest -s tests/

# 运行指定测试文件
pytest -s tests/test_tool_validation.py

# 运行匹配特定模式的测试
pytest -k "test_heartbeat" tests/

# 以详细模式运行
pytest -v tests/

# 运行并生成覆盖率报告(需安装 coverage)
pytest --cov=nanobot tests/

测试组织结构

测试文件

文件

测试内容

对应组件

test_tool_validation.py

工具参数校验、JSON Schema 检查

agent/tools/base.pyagent/tools/registry.py

test_heartbeat_service.py

Heartbeat 幂等性、LLM 决策处理

heartbeat/service.py

test_cron_service.py

定时任务调度、触发、取消

cron/service.py

test_cron_commands.py

定时任务命令解析与分发

cron/

test_commands.py

CLI 命令解析

cli/commands.py

test_cli_input.py

CLI 输入处理

cli/commands.py

test_context_prompt_cache.py

Prompt 缓存优化

agent/context.py

test_loop_save_turn.py

Agent 循环轮次保存

agent/loop.py

test_message_tool.py

消息工具行为

agent/tools/message.py

test_message_tool_suppress.py

消息抑制逻辑

agent/tools/message.py

test_email_channel.py

邮件通道解析

channels/email.py

test_matrix_channel.py

Matrix 通道集成

channels/matrix.py

test_feishu_post_content.py

飞书消息格式化

channels/feishu.py

test_memory_consolidation_types.py

记忆整合边界情况

agent/memory.py

test_consolidate_offset.py

整合偏移量追踪

agent/memory.pysession/manager.py

test_task_cancel.py

任务取消逻辑

cron/service.py

测试模式

测试替身

代码库使用简单的自定义测试替身,而非重型 mock 框架:

DummyProvider — 一个返回预配置响应的最小化 LLM provider:

class DummyProvider:
    def __init__(self, responses: list[LLMResponse]):
        self._responses = list(responses)

    async def chat(self, *args, **kwargs) -> LLMResponse:
        if self._responses:
            return self._responses.pop(0)
        return LLMResponse(content="", tool_calls=[])

SampleTool — 一个用于测试参数校验的最小化工具:

class SampleTool(Tool):
    @property
    def name(self) -> str:
        return "sample"

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "query": {"type": "string", "minLength": 2},
                "count": {"type": "integer", "minimum": 1, "maximum": 10},
            },
            "required": ["query", "count"],
        }

    async def execute(self, **kwargs: Any) -> str:
        return "ok"

异步测试

所有异步测试使用 @pytest.mark.asyncio 装饰器,并可通过 tmp_path 实现文件系统隔离:

@pytest.mark.asyncio
async def test_heartbeat_decide_skip(tmp_path) -> None:
    provider = DummyProvider([LLMResponse(content="no tool call", tool_calls=[])])
    service = HeartbeatService(workspace=tmp_path, provider=provider, model="test")
    action, tasks = await service._decide("heartbeat content")
    assert action == "skip"

文件系统隔离

涉及文件系统操作的测试使用 pytest 的 tmp_path fixture:

@pytest.mark.asyncio
async def test_session_persistence(tmp_path) -> None:
    manager = SessionManager(tmp_path)
    session = manager.get_or_create("test:123")
    session.add_message("user", "Hello")
    manager.save(session)

    # 从磁盘重新加载
    manager2 = SessionManager(tmp_path)
    session2 = manager2.get_or_create("test:123")
    assert len(session2.messages) == 1

校验测试

工具参数校验通过各种边界情况进行了充分测试:

def test_validate_params_missing_required() -> None:
    tool = SampleTool()
    errors = tool.validate_params({"query": "hi"})
    assert "missing required count" in "; ".join(errors)

def test_validate_params_nested_object_and_array() -> None:
    tool = SampleTool()
    errors = tool.validate_params({
        "query": "hi", "count": 2,
        "meta": {"flags": [1, "ok"]},
    })
    assert any("missing required meta.tag" in e for e in errors)
    assert any("meta.flags[0] should be string" in e for e in errors)

已覆盖的测试内容

核心 Agent

  • 工具参数校验:JSON Schema 校验(类型、范围、枚举、嵌套对象、数组、必填字段)

  • 工具注册表:工具查找、缺失工具的错误信息、校验错误传播

  • Agent 循环:轮次保存、会话持久化

  • 上下文构建:Prompt 缓存优化

记忆与会话

  • 记忆整合:LLM 驱动的整合、偏移量追踪、不同参数类型的边界情况

  • 会话管理:消息追加、历史记录检索、孤立工具结果清理

通道

  • Email:IMAP 消息解析、SMTP 格式化

  • Matrix:房间事件处理

  • 飞书:富文本消息格式化

服务

  • Heartbeat:幂等启动、跳过/执行决策、LLM 工具调用处理

  • Cron:任务调度(at/every/cron)、触发、取消、存储持久化

CLI

  • 命令解析:Typer 命令注册

  • 输入处理:终端输入、退出命令

未覆盖的测试内容(已知空白)

  • 端到端流程:没有使用真实 LLM provider 运行完整 gateway 的集成测试

  • 通道连接:没有测试实际的 WebSocket/API 连接(需要外部服务)

  • LLM 响应质量:没有测试实际的 LLM 输出(需要 API key)

  • 并发访问:没有针对多通道同时发送消息的压力测试

  • Provider 特定行为:没有测试 LiteLLM 前缀、环境变量注入或 gateway 检测

添加新测试

命名规范

tests/test_{module_or_feature}.py

模板

"""Tests for {feature}."""

import pytest

from nanobot.module import SomeClass


def test_descriptive_behavior_name() -> None:
    """Test that SomeClass does X when Y."""
    obj = SomeClass()
    result = obj.method()
    assert result == expected


@pytest.mark.asyncio
async def test_async_behavior(tmp_path) -> None:
    """Test async operation with filesystem isolation."""
    obj = SomeClass(workspace=tmp_path)
    result = await obj.async_method()
    assert result is not None

最佳实践

  1. 保持测试快速:不发起网络请求、不使用 sleep、不做重量级 I/O

  2. 使用 tmp_path:绝不写入真实的文件系统路径

  3. 最小化测试替身:使用返回预配置数据的简单类

  4. 聚焦单一断言:每个测试只验证一个特定行为

  5. 描述性命名test_{被测内容}_{条件}_{预期结果}

相关文档


最后更新:2026-03-15 版本:1.0