Zulip 自动化测试体系完全指南:从 test-all 到单测隔离策略
【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip
Zulip 是少数把测试套件当作第一等基础设施来建设的开源项目:它的后端测试套件test-backend可以在约一分钟内跑完约 98% 行覆盖率(核心代码 100%)的完整测试,前端 Node 单测套件在 10 秒内完成。本文以 docs/testing/testing.md 为骨架,结合仓库内测试工具链源码,系统讲解 Zulip 测试体系的分层结构、每个套件的运行方式与核心参数、以及"测试套件禁止访问外网"这一关键设计策略,帮助你在本地开发环境中快速定位并运行与改动相关的测试子集。
测试体系概览:为什么 Zulip 敢于"快速前进而不破坏功能"
Zulip 的工程策略可以概括为"move quickly without breaking things"(快速前进而不破坏功能)。支撑这一策略的,正是多年系统化投入构建的测试、工具链与开发规范。在 docs/testing/philosophy.md 中,项目明确记录了这一思路:测试套件的性能与可靠性是维护者视为优先级的核心资产。当套件变慢或变得不确定(flaky)时,开发者会本能地回避运行测试,进而回避改进测试,最终整个代码库的测试质量会螺旋式恶化——这是大型软件项目常见的"测试腐烂"(test rot)命运,Zulip 的目标正是避免它。
因此项目制定了两个硬性性能指标:
- 完整后端套件
test-backend的目标运行时间:约 1 分钟; - 完整前端套件
test-js-with-node的目标运行时间:10 秒以内。
与之配套的工程手段包括:测试绝不访问外网(详见下文"网络隔离策略");通过唯一随机前缀隔离不同测试对 Redis、memcached 的键污染;每个测试用例运行在数据库事务中,测试结束后回滚事务;每个测试进程只与一份专用的模板测试数据库交互,进程结束即销毁。对于任何非确定性失败的测试,Zulip 会像对待产品级 bug 一样严肃调查。
运行测试:从全量 test-all 到秒级单测
全量运行:./tools/test-all
Zulip 的所有测试都必须运行在 Zulip 开发环境内(若使用 Vagrant 需先vagrant ssh进入)。运行全部套件(与 CI 的覆盖范围相当)只需:
./tools/test-all从 tools/test-all 的源码可以看到,它会依序执行后端组与前端组的十余个检查项:先是./tools/check-provision预检环境,然后运行后端 lint、test-tools、带--include-webhooks --ban-console-output的完整test-backend、test-migrations、帮助中心文档检查、API 文档实测、uv lock --check依赖锁定校验;前端部分则运行前端 lint、test-js-with-node、schema 校验、i18n 与大小写规范检查、Puppeteer 端到端测试等(optimize-svg、test-run-dev、test-queue-worker-reload等低频检查被注释为"低变更频率"而未纳入本地全量)。
工具自身也在输出中明确提醒:test-all很慢,推荐只运行相关的子套件,依赖 CI 跑完整套件,以获得快速的编辑-测试循环。
开发时的秒级迭代
活跃开发时,编辑/刷新循环通常只用下面这些秒级命令:
./tools/lint zerver/models/__init__.py # 只 lint 你刚改动的文件 ./tools/test-backend zerver.tests.test_markdown.MarkdownEmbedsTest.test_inline_youtube ./tools/test-backend MarkdownEmbedsTest # 更多选项见 `test-backend --help` ./tools/test-js-with-node util每个工具都提供--help查看完整选项。例如 tools/test-backend 支持按"点分路径"(dotted.test.name)精确定位单个测试类或方法,也支持只传类名;./tools/test-backend --help是学习这些选项的最佳途径。
四大主要测试套件
Zulip 有四大主要测试套件,每个开发者都会接触,各自有独立文档:
| 套件 | 命令 | 测试对象 | 详情文档 |
|---|---|---|---|
| Linters(十余个并行 lint 器) | ./tools/lint | Python/JS/TS/模板/CSS/JSON/Markdown/Puppet/shell 等 | docs/testing/linters.md |
| Django 后端测试 | ./tools/test-backend | 服务端 Python 代码,基于django.test | docs/testing/testing-with-django.md |
| Node 前端单测 | ./tools/test-js-with-node | 经 node.js 运行的 JavaScript/TypeScript 前端逻辑 | docs/testing/testing-with-node.md |
| Puppeteer 端到端测试 | ./tools/test-js-with-puppeteer | 通过真实 Chromium 浏览器驱动的黑盒 UI 测试 | docs/testing/testing-with-puppeteer.md |
Linters:./tools/lint
./tools/test-all会自动运行 lint;也可单独运行或只检查指定文件:
./tools/lint ./tools/lint web/src/compose.ts ./tools/lint web/src/./tools/lint内部通过 fork 子进程并行执行多项检查。常用选项包括:--fix(让支持自动修复的 linter 直接修掉基础问题)、--verbose(给出--fix覆盖不到的常见错误的修复指引)、--skip/--only(只运行部分 linter)、-m(只检查已修改文件)。项目还提供 Git pre-commit 钩子(见 docs/git/zulip-tools.md),提交时几百毫秒内自动对改动文件跑tools/lint。注意:linter 只检查 Git 已跟踪的文件,新增文件需先git add。
lint 检查覆盖的范围相当广:Python 用 Ruff(配置在pyproject.toml的[tool.ruff]段,见 pyproject.toml),JS/TS 用 ESLint,CSS/JS/TS/YAML 格式用 Prettier,Puppet 清单用官方 validator,Django/Handlebars 模板有自研的标签配对与缩进校验(驱动脚本为 tools/check-templates,引擎为 tools/lib/template_parser.py),Python 静态类型用 mypy、TS 编译用 tsc;此外还有针对行尾空白、tab 字符、Python 行长的通用检查,以及大量自研的正则规则(集中于 tools/linter_lib)。特殊的#ignorelinelength注释可豁免超长行(例如注释中极长的 URL)。
Django 后端测试:./tools/test-backend
后端测试全部位于zerver/tests/目录,基于 Django 的django.test框架,在开发环境内多线程并行运行。迭代模式下按点分约定运行单个测试或模块:
./tools/test-backend zerver.tests.test_queue_worker.WorkerTest常用选项包括:
--verbose:详细输出;-h可发现覆盖率、URL 覆盖率、慢测试等辅助功能;--profile:对测试做性能剖析;--stop(或-x):在第一个错误后停止;--rerun:只重跑上次失败的测试;--coverage:生成覆盖率 HTML 报告(工具会打印开发环境内可访问的报告 URL),报告还会显示每行被哪些测试执行,便于反查已有测试;且test-backend --coverage会断言一批指定文件必须保持 100% 覆盖率,否则报错;--ban-console-output:检查是否存在杂散print,规范的测试不应向控制台输出任何内容;--include-webhooks:默认不带参数的test-backend会跳过zerver/webhooks/下的 webhook 集成测试(它们约占全部后端测试 25% 的运行时间),CI 始终用--include-webhooks全量运行;本地改 webhook 时建议直接test-backend zerver/webhooks。
编写测试时,核心基类是 zerver/lib/test_classes.py 中的ZulipTestCase(以及 webhook 专用的WebhookTestCase),辅助函数集中在 zerver/lib/test_helpers.py。所有测试共享同一份莎士比亚角色命名的 fixture 用户数据(隶属 "zulip.com" realm,生成逻辑见 tools/setup/generate-fixtures),测试内的数据库写入发生在事务中、结束后回滚。常用测试策略包括:针对 URL 端点的 endpoint 测试(self.login()+client_get/client_post+assert_json_success/assert_json_error组合)、库函数单测、JSON fixture 驱动的第三方集成测试(fixtures 存于zerver/tests/fixtures)、用queries_captured()/assert_database_query_count防止 N+1 查询的 SQL 性能测试、验证事件系统数据一致性的BaseAction.do_test()、以及验证未授权访问被正确拒绝的 negative tests。模板渲染健康检查见 zerver/tests/test_templates.py。
Node 前端单测:./tools/test-js-with-node
这是 Zulip 测试 JS/TS 前端代码的首选方式,比 Puppeteer 黑盒测试快 100 倍以上且更容易写对。测试文件位于web/tests/(如web/tests/stream_data.test.cjs),测试名与web/src下的被测模块一一对应。运行:
tools/test-js-with-node tools/test-js-with-node --coverage # 生成覆盖率报告与 Puppeteer 不同,Node 单测没有浏览器、不连接服务器、通常也不使用完整虚拟 DOM(仅少量测试用jsdom)。DOM 操作(Zulip 中几乎全部经 jQuery 完成)通过自研的约 500 行库 web/tests/lib/zjquery.cjs 进行"stub"——它提供大部分 jQuery DOM 操作的最小实现,例如$obj.set_find_results(selector, $value)可让$obj.find(selector)返回预设值;遇到未 stub 的调用会报Error: You must create a stub for $("#foo").bar。它的单测文件 web/tests/zjquery.test.cjs 同时充当使用文档。
依赖处理上,测试文件会显式声明模块:zrequire('util')载入真实模块代码;mock_esm("../../web/src/reminder", {...})整体 stub 掉某个模块;也可以先zrequire('narrow_state')再在测试内覆盖个别函数,实现"借用真实功能 + 局部 stub"的混合模式。测试运行器 web/tests/lib/index.cjs 会自动运行web/tests下所有.test.cjs文件,因此新建测试只需在该目录新增文件。此外,mock_template工具用于校验 Handlebars 模板(位于web/templates)被调用时传入的 context 数据是否正确,可按需选择是否真实渲染模板。覆盖率报告中完全未被测试的模块不列出,因此整体数字偏乐观,但单文件数据是准确的,分支覆盖率达到 80% 是合理目标。
Puppeteer 端到端测试:./tools/test-js-with-puppeteer
部分代码(如登录导航、剪贴板、键盘快捷键等浏览器行为交互)必须用真实浏览器验证:
tools/test-js-with-puppeteer测试文件位于web/e2e-tests/,公共辅助函数在 web/e2e-tests/lib/common.ts。它们连接真实 Chromium 浏览器与真实 Zulip 开发服务器(服务器使用zproject/test_extra_settings.py中的隔离数据库配置),是典型的黑盒测试——步骤就是用户会做的操作("按下这个键""等待这个元素出现""点击这个元素")。例如测试x快捷键打开私信撰写框:
async function test_private_message_compose_shortcut(page) { await page.keyboard.press("KeyX"); await page.waitForSelector("#private_message_recipient", {visible: true}); await common.pm_recipient.expect(page, ""); await close_compose_box(page); }waitForSelector是这类测试的命门:缺失或等待对象错误的 wait 会造成非确定性失败,且必须使用{visible: true}选项(否则元素只要存在于 DOM 就停止等待,对"常驻 DOM、用显示/隐藏切换"的 UI 完全无效)。调试工具包括:断言失败时自动生成var/puppeteer/*.png截图、console.log打印调试、以及在慢速 CI 机器上"本地能过、CI 必挂"时优先怀疑 wait 缺失。写完新测试后,项目要求循环运行 100 次以确认无 flaky 风险。由于黑盒测试慢、维护成本高且易产生非确定性失败,Zulip 的策略是"两者皆可时优先写 Node 单测"。
其余十余个小套件:各有其不可替代的价值
除四大主套件外,Zulip 还有约十几个小型测试套件。它们大多因为性能或"需要动环境"的原因没有并入主套件,但各自守着一类特殊 bug:
./tools/test-migrations:校验zerver/migrations中的数据库迁移内容与zerver/models/*.py定义的模型一致,保证迁移不会漏改模型(参见 docs/subsystems/schema-migrations.md);./tools/test-documentation:检查 ReadTheDocs 站点文档中的断链;./tools/test-help-documentation:检查/help/帮助中心文档及相关页面的断链;./tools/test-api:实测/api的 API 文档"真的能用",其代码定义在zerver/openapi/python_examples.py;./tools/check-capitalization:检查所有面向用户的翻译字符串是否符合 Zulip 的大小写规范,依赖tools/lib/capitalization.py中的专有名词豁免清单(IGNORED_PHRASES);./tools/check-frontend-i18n:检查 Handlebars 模板中一个常见 bug——翻译含变量的代码块时用错了语法;./tools/test-run-dev:验证run-dev能正常启动,防止开发环境被改坏;./tools/test-queue-worker-reload:验证 Zulip 的队列处理器在代码变更后能正确重载;./tools/setup/optimize-svg:检查所有集成 logo 的 SVG 图形是否已优化压缩(第三方 logo 不便修改,此检查防止代码库无限膨胀);./tools/test-tools:为开发工具链(主要是各类 linter)编写的自动化测试,它们不用于生产环境。
网络隔离策略:测试套件绝不访问外网
这是 Zulip 测试体系中最具特色的设计决策,也是原文档重点阐述的部分。作为政策,Zulip 的测试套件绝不允许发起任何外出的 HTTP 或其他网络请求,原因有二:
- 会发外网请求的测试在用户离线时会失败;
- 更隐蔽的是,这类测试隐式依赖第三方服务的可用性——一旦对方临时故障,测试就会非确定性地失败,白白消耗大量开发者时间。
因此,测试中对任何 Zulip 代码路径可能发出的外网请求,一律用mock把响应"硬编码"进测试。实现上并不复杂:使用测试 fixture(即测试用的固定数据)配合 Python 的mock.patch函数,为每一个外发请求指定应有的 HTTP 响应。Zulip 还大量使用responses库来 mock 掉requests调用(可用git grep responses.add找到示例),具体手法见 docs/testing/testing-with-django.md 的 "Zulip mocking practices" 一节;低层HttpRequest场景则用 Zulip 自有的HostRequestMock类。
这套政策在后端主套件中是被强制执行的:tools/test-backend 通过覆盖外出 HTTP 代码路径常用的库函数(httplib2.Http().request、requests.request等)让它们直接抛异常。从源码看,其实现是block_internet()上下文管理器(tools/test-backend):它把responses.ConnectionError替换为自定义的ZulipInternetBlockedError,并挂上responses.RequestsMock(),整个测试运行期间任何未注册的 URL 访问都会触发该异常;同时脚本还会移除http_proxy/https_proxy环境变量,杜绝代理绕行。这种强制虽不穷尽(Python 中还有很多其他方式触网),但足以拦截绝大多数"新代码依赖外网"的常见情况,报错形如:
File "tools/test-backend", line 120, in internet_guard raise Exception("Outgoing network requests are not allowed in the Zulip tests." Exception: Outgoing network requests are not allowed in the Zulip tests. ...唯一的例外:文档测试
文档类测试是上述政策的唯一例外——要验证文档链接是否有效,除了真的去访问它们别无他法,因此这类测试确实会偶发非确定性失败。项目采取的折中方案是:在 CI 中,文档测试只验证 Zulip 项目自己控制的网站(zulip.com、Zulip 的 GitHub、ReadTheDocs),不去验证第三方网站链接。这从 tools/test-all 中也可印证——本地全量运行使用--skip-external-links跳过外部链接检查。
手动测试与端到端哲学
自动化覆盖不了的部分(如 CSS 视觉效果、Markdown 特殊排版),Zulip 依赖手动测试(manual QA)兜底。手动测试的建议见 docs/testing/manual-testing.md:优先抓安全/权限类 bug;用隐身窗口模拟多用户;始终开着 inspector 控制台观察告警;习惯用 Cordelia 作主用户、Hamlet 作对话对象、Iago 测管理员功能。与本文主题相关的开发环境使用细节见 docs/development/using.md。
在测试编写哲学上(详见 docs/testing/philosophy.md),Zulip 反对为内部小函数堆砌大量"纯单测"——那会让重构背上沉重的测试修改成本。更优的做法是针对你承诺保持稳定的接口写测试:Zulip 的 Web 应用、移动端与第三方客户端共用同一套 API,因此绝大多数服务端测试直接打 Zulip 端到端 API;webhook 测试则用从真实第三方服务抓取的 payload 打向 webhook 端点并断言输出消息。同时,有安全影响的代码绝不重复实现——像access_stream_by_id这类权限校验函数被精心编写并集中测试,所有可能向用户泄露数据的代码路径都必须经由这些函数取数(每个条件独立成行以便覆盖率工具验证),这是把"安全逻辑 bug"系统性消灭在结构层面的关键手段。
与 CI 的衔接
.github/workflows/zulip-ci.yml 是主 CI 工作流:在全部受支持平台上运行各主套件,其中 Puppeteer 与文档测试只在单个平台运行(它们慢且与基座 OS/Python 版本关联度低);production-suite.yml则构建发布 tarball 并在全新容器中安装后跑 Nagios 等检查。tools/ci下的代码是测试套件与 CI 之间的薄封装。CI 同样坚持"本地秒级可复现"原则——任何 CI 行为都应有本地 1 分钟内(最好 3 秒内)的运行方式,详见 docs/testing/continuous-integration.md。
快速上手指南
给初次接触 Zulip 测试体系的开发者一份速查清单:
- 只改了一个 Python 文件?跑
./tools/lint <文件>加./tools/test-backend指定测试; - 改了前端 JS/TS 逻辑?跑
./tools/test-js-with-node(可--coverage); - 动了浏览器交互或 UI 流程?跑
./tools/test-js-with-puppeteer; - 所有测试都遵守"无外网"铁律,网络相关场景一律用
mock.patch/responses造 fixture; - 提交前用 Git pre-commit 钩子自动 lint,最终交给 CI 全量把关(docs/testing/continuous-integration.md)。
【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考