things-mcp 测试体系拆解:192 个单元测试,外加一份让 Claude 自动执行的集成测试计划

原创2026-11-12 23:59:181,868 阅读

things-mcp 测试体系拆解:192 个单元测试,外加一份让 Claude 自动执行的集成测试计划

things-mcp 是一个 Things.app MCP Server,让 Claude Desktop、ChatGPT 等 AI 客户端直接读写 macOS 上 Things 3 的任务数据。今天拆解它的测试体系:192 个 pytest 单元测试 + 一份让 Claude 自动执行的集成测试计划,两层互补,覆盖从 URL 拼接到真实 Things 数据库的全链路。

先认识被测对象:三个核心模块 🧩

things-mcp 的源码非常小巧,全部逻辑集中在 src/things_mcp/ 下的三个文件:

模块 职责 测试重点
server.py 26 个 MCP 工具的定义、分页、Someday 过滤 工具返回文本与结构化数据
url_scheme.py Things URL 拼接、AppleScript 执行 URL 编码、转义、回退逻辑
formatters.py 把数据库对象格式化为可读文本 嵌套结构、时间戳、任务龄

测试体系就围绕这三个模块展开,再加上一个"文档级"的集成测试计划,构成两层防线:

things-mcp 测试金字塔
┌─────────────────────────────┐
│ 集成测试计划(Claude 执行)    │  真实 Things 数据库
├─────────────────────────────┤
│ 192 个 pytest 单元测试        │  全 mock,无需 macOS
└─────────────────────────────┘

六大测试文件的分工:192 个测试各自守什么 📊

单元测试全部位于 tests/ 目录,共 6 个测试文件,分工明确:

测试文件 测试数 覆盖目标
test_formatters.py 79 格式化函数:todo/project/area/tag/heading、任务龄计算、时间戳
test_url_scheme.py 48 URL 构建、osascript 调用回退、AppleScript 转义
test_things_server.py 46 服务器工具:分页、structured_content 结构、Logbook 解析
test_someday_filtering.py 8 Someday 项目过滤的核心函数
test_mcp_server_filtering.py 7 MCP 工具层的过滤集成行为
test_things_server_headings.py 4 项目标题(headings)工具

合计正好 192 个(CLAUDE.md 中也有同样记录)。可以看到测试数量与模块复杂度成正比:格式化逻辑最复杂,测试也最多;headings 功能较薄,4 个测试即可覆盖。

一键运行:things-mcp 单元测试怎么跑 🏃

测试依赖独立声明为可选依赖(pyproject.toml 中的 test extra),pytest 配置也在同一文件里(pyproject.toml),开箱即用:

# 安装含测试依赖的环境
uv sync --extra test

# 运行全部测试
uv run pytest

# 详细输出 / 只跑某个文件 / 按名称匹配
uv run pytest -v
uv run pytest tests/test_url_scheme.py
uv run pytest -k "test_add_todo"

一个值得注意的配置是 asyncio_mode = "auto"——MCP 工具函数是异步的,开启该模式后测试无需逐个标注 @pytest.mark.asyncio 也能直接测协程。

关键设计 1:全量 Mock,让测试在没有 Things 的机器上跑 🧪

things-mcp 依赖 macOS、Things 3 数据库和 things.py 库,单元测试却能在任何 Linux CI 上运行,诀窍是把三个外部边界全部 mock 掉:

  • 数据库读取:patch('things.get')、patch('things.todos') 等拦截所有数据访问;
  • Token 获取:patch('things.token') 返回固定 token,连认证都不需要真实环境(tests/conftest.py);
  • 系统调用:写操作走 subprocess.run 执行 osascript,测试直接 mock 该调用,只断言参数是否拼接正确(tests/test_url_scheme.py)。

例如执行 URL 的测试,断言的就是最终传给 osascript 的命令数组,连 shell 回退路径都单独覆盖。

关键设计 2:9 个共享 fixture + 1 个归一化助手 📦

公共测试数据集中在 tests/conftest.py,提供 9 个 fixture:

  • 数据类:mock_todo(含 checklist、tags 的完整任务)、mock_completed_todo、mock_project、mock_area、mock_tag;
  • mock 类:mock_things_token、mock_things_get(按 UUID 返回不同结果)、mock_things_todos、mock_things_projects。

另有一个 13 行的小助手 tests/_helpers.py 中的 tool_text():因为 read 工具同时返回"人类可读文本 + structured_content"双通道,有时又是纯字符串,tool_text() 把两种形态归一化为文本,让断言代码不用关心返回形态。

💡 小文件大作用:这种"抹平返回值差异"的助手,是双通道 API 测试中最容易被忽略的一环。

关键设计 3:给"最容易被 UI 骗"的业务规则留了双份测试 🎯

things-mcp 有个不显眼的业务规则:Someday 项目里的任务不应出现在 Today / Upcoming / Anytime 视图(与 Things 原生 UI 行为一致)。这类"看起来对"的过滤逻辑最容易回归出错,作者用了两层测试:

  1. 函数层:test_someday_filtering.py 直接测 filter_someday_project_tasks,mock 掉项目列表,验证 Someday 任务被剔除、正常任务保留;
  2. 工具层:test_mcp_server_filtering.py 测真实的 MCP 工具(如 get_anytime),构造"3 个任务分别属于 Someday 项目 / 活跃项目 / 无项目"的场景,断言只剩后两者。

两层各 8 个和 7 个测试,函数改错了工具层会红,工具组装错了函数层测不出来——互为兜底。

集成测试计划:一份写给 Claude 的"操作手册" 🤖

单元测试无法验证真实链路:Things URL 是否真的被 App 执行、视图隔离在真实数据库上是否成立。传统做法是写 Selenium/AppleScript 脚本,而 things-mcp 的做法更有意思——把集成测试写成一份结构化的 Markdown 计划,交给 Claude(Claude Cowork 或 Claude Code)通过 MCP 工具自己执行:

📄 docs/mcp_integration_test_plan.md(523 行,7 个阶段)

阶段 内容 检查项
Phase 1 只读工具验证 全部列表/查询/搜索工具可响应;分页 limit/offset 双通道一致;越界 offset 单独提示(计划 1.4 节)
Phase 2 创建测试数据 1 个项目(含 2 任务)+ 3 个任务 + 1 个 Area,共 7 项,记录所有 UUID
Phase 3 验证创建结果 搜索命中、Someday 可见、Today/Upcoming/Anytime 隔离、提醒任务落入 Upcoming
Phase 4 更新操作 更新 todo/项目/Area;checklist 省略时保留、空列表时清空
Phase 5 UI 导航工具 show_item 打开 Today 视图、search_items 呈现搜索结果
Phase 6 清理 全部标记 canceled,搜索应查无结果;Area 需人工删除并登记
Phase 7 人工核对清单 Logbook 状态确认、可选永久删除、Area 手动删除

安全规则:这套计划为什么敢碰真实数据 🔒

计划开头专门定义了 6 条安全规则:

  • 所有测试项必须带 [MCP-TEST] 前缀,一眼可辨;
  • 一律使用 when: "someday",绝不污染用户的 Today/Upcoming 列表(短暂的正向存在性测试除外,测完立即移回 Someday);
  • 绝不修改用户已有数据,只操作自己创建的项;
  • 全程追踪 UUID,Phase 6 强制清理——即使前面阶段失败也必须执行清理;
  • 已知 add_area 无法通过 MCP 删除(Things 中删 Area 会连带删项目,属设计决策),所以测试 Area 明确标注"需人工删除"并在 Phase 7 列出。

一个细节:连 2048 年都不能用 📅

计划里还埋了一个冷门知识点(Phase 2.4 注释):Things 用打包整数存储日期,年份字段上限是 2047。所以提醒任务不能用 2099 这类"哨兵日期",虽然排序上仍是"未来",但 things.py 解不出来,结构化字段会变 null。测试统一改用 2030 年——这类坑只有对着真实数据库反复测试才会暴露,也正是这份集成计划的价值所在。CHANGELOG.md 中多个 bug fix(Logbook 漏 canceled 项、get_recent 时区偏移、checklist 空列表不生效)都能对应到计划里的检查项。

这套体系对普通开发者的 3 点启发 ✅

  1. 分层克制:核心函数用函数层测试保证确定性,业务行为再用工具层测试兜底,避免"一个测试文件测所有东西"的泥潭;
  2. 把环境依赖 mock 在边界上:只 mock things.* 和 subprocess.run 这两个外部入口,内部逻辑全走真实代码;
  3. 没有 CI 客户端?让 AI 当测试执行器:当被测对象必须通过 MCP 协议调用时,把测试写成"带勾选框 + 安全规则 + 完成条件"的文档,交给 AI 逐步执行并回报,是一种低成本且可维护的集成验证方式。

相关路径速查 📁

资料 路径
单元测试目录 tests/
共享 fixture tests/conftest.py
双通道归一化助手 tests/_helpers.py
集成测试计划 docs/mcp_integration_test_plan.md
pytest 配置 pyproject.toml
AI 协作开发指引 CLAUDE.md
版本与修复记录 CHANGELOG.md

想本地体验完整流程,可直接克隆仓库 https://gitcode.com/gh_mirrors/th/things-mcp,然后 uv sync --extra test && uv run pytest,30 秒内就能看到 192 个测试全绿的输出。

登录后查看全文
things-mcp