ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Zulip 测试哲学:让自动化测试成为大型开源项目快速迭代的引擎

2026/9/12 18:27:26 拓冰建站 浏览量
Zulip 测试哲学:让自动化测试成为大型开源项目快速迭代的引擎 Zulip 测试哲学让自动化测试成为大型开源项目快速迭代的引擎【免费下载链接】zulipZulip server and web application. Open-source team chat that helps teams stay productive and focused.项目地址: https://gitcode.com/GitHub_Trending/zu/zulip本篇技术指南以 Zulip 仓库中的 docs/testing/philosophy.md 为骨架系统梳理 Zulip 团队设计自动化测试套件的核心原则如何用可靠、快速、易扩展的测试支撑快速移动而不破坏任何东西的工程策略如何在集成测试 vs 单元测试之间做出正确取舍以及如何通过共享测试代码与安全访问函数设计来减少重复劳动与安全漏洞。读完本文你将理解 Zulip 整套测试工具链tools/test-backend、tools/test-js-with-node、zerver/lib/test_runner.py背后的设计动机并可直接把这些方法论移植到自己的项目中。有效的测试让我们能够快速移动Zulip 的工程策略可以概括为快速移动而不破坏任何东西move quickly without breaking things。尽管 Zulip 维护者每天要评审大量来自新贡献者、对其改动代码缺乏深入理解的提交但维护者把集成改动的大部分时间花在产品决策和代码结构/可读性上而不是正确性、风格或底层问题上。之所以能做到这一点是因为项目多年来系统性地投资于测试、工具链、代码结构、文档和开发实践确保贡献者写出的代码在提交评审前就已相对正确。测试环节的职责是提供可靠、广泛、易于扩展的测试套件覆盖大多数 bug 类别把人工验证和排查改动是否正确的时间降到最低。Zulip 的开发环境认证测试设施是一个典型例证它允许在自动化测试和开发环境中使用mock LDAP 数据库fakeldap让重构和改进认证这一重要模块变得极其容易——而在过去你需要先搭一个真正的 LDAP 服务器并灌入测试数据才能测试 LDAP 认证。从源码看这个能力由 zerver/lib/test_helpers.py 中的MockLDAP(fakeldap.MockLDAP)类提供开发环境侧则通过zproject/dev_settings.py中的FAKE_LDAP_MODE取值为a/b/c对应三种邮箱与用户名的映射关系和FAKE_LDAP_NUM_USERS默认 8 个用户来开关与配置。当然并非 Zulip 的每个组件都有出色的测试套件但许多组件都有。对这些组件而言新贡献者往往能做出实质性修改且在提交评审时大体正确更重要的是维护者把原本用于验证改动正确的时间节省下来转而专注于代码库的可读性、良好结构和良好测试。测试套件的性能与可靠性是生死攸关的当自动化测试套件变慢或不可靠时开发者会逃避运行它们也会逃避改进它们无论是改进系统还是单个测试。而让测试变慢或变不可靠的改动往往是其他开发的意外副作用因此这类问题会随代码库增长而不断累积。如果不刻意防止任何大型软件项目最终都会把测试套件腐烂成又慢、又不可靠、不值得信任、人人讨厌的状态——Zulip 的目标就是避免这一命运。Zulip 把每个自动化测试套件都必须既快又可靠在 CI 和本地都能通过视为必要前提。具体的性能目标是测试套件命令性能目标完整后端套件tools/test-backend约 1 分钟内完成完整前端套件tools/test-js-with-node10 秒以内完成达成性能目标的关键技术原文档重点强调了以下几项技术仓库源码给出了具体实现证据测试套件被设计为不访问互联网。tools/test-backend用block_internet()上下文管理器见 tools/test-backend包裹全部测试它通过mock.patch.object(responses, ConnectionError, newZulipInternetBlockedError)和responses.RequestsMock()一旦测试代码尝试访问未注册的 URL就会抛出ZulipInternetBlockedErrortools/test-backend提示Zulip 测试中不允许发起外发网络请求。凡测试需要用到出站 HTTP 请求的地方一律用responses这类库 mock 掉响应。严格避免测试间数据污染PostgreSQL、Redis、memcached 等每个测试用例在访问 Redis 和 memcached 时都会在其使用的所有 key 前加上唯一随机前缀。对应实现是 zerver/lib/test_runner.py 的get_database_id()它基于每次test-backend调用开始时生成的random.randint(1, 10000000)随机数来保证进程间 ID 唯一。每个测试用例都运行在数据库事务中测试结束后事务被回滚abort。每个测试进程只与一个专用模板数据库zulip_test_template的全新副本交互进程结束后该副本被销毁。这套逻辑分布在 zerver/lib/test_runner.pydestroy_test_databases/create_test_databases并行模式下每个 worker 通过clone_test_db(suffixdatabase_id)克隆自己的库与 zerver/lib/test_fixtures.pyBACKEND_DATABASE_TEMPLATE与update_test_databases_if_required中并行 worker 的初始化见 zerver/lib/test_runner.py 的init_worker。对非确定性间歇性失败的测试按产品级 bug 的优先级严格调查绝不放过。此外tools/test-backend还提供--rerun只重跑上一次失败的测试读取var/last_test_failure.json、--coverage计算并强制覆盖率、-x/--stop首个失败即停止、--parallel N并行进程数默认等于逻辑 CPU 数等参数运行时还会调用tools/webpack --test预先构建测试所需的前端产物。这些细节共同保证了1 分钟跑完仍然能提供足够可信的反馈。集成测试还是单元测试开发者经常纠结该写集成测试还是单元测试。Zulip 的观点是测试应该针对你已经在依赖其保持稳定的接口来写——也就是你自己或他人已经在指望它除了兼容性变更外基本不变的那些接口。针对 Zulip 的端到端 API 写服务端测试就是一个绝佳范例大量代码都是基于 Zulip API 编写的这些代码都指望 API 大体上继续按它们使用的方式工作。即使 API 的唯一用户只是本项目自己的客户端如移动 App也成立——因为已有大量已安装的移动 App 副本在指望 API 不会突然发生不兼容的变化。为什么面向接口测试如此重要针对接口写测试有一个重要原因这些测试会成为你每次改动该接口时的成本——你得去更新一大批测试。在大代码库里如果有很多针对小型内部函数的单元测试那么每次你重构并改变内部接口时——即使这些接口完全是你自己发明的、完全内部、改了也不会破坏任何东西——你也得去编辑一堆测试来适配新接口。认真对待测试时这尤其费劲因为你要判断测试失败到底是不是在告诉你该听的东西。这会导致测试感觉像无意义的杂活busywork开发者也就不愿用心维护和扩展测试套件。但如果你的测试是针对外部 API写的那么你做了某个重构改动后一批测试失败……这是在告诉你非常真实的东西你当然可以改测试但测试是那些你够不着的真实用户和真实代码的替身它们会以同样的方式坏掉。于是你仍然可以改动但必须为外面那些真实用户想好迁移或向后兼容策略——改测试反而是其中最简单的一步。同时对测试的改动也是对代码评审者的友好提醒你改动了某个接口评审者需要仔细思考这些接口变更是否会破坏既有客户端、是否已反映在接口文档中。这一哲学在不同项目中的形态如果是一个以 API 为主的 Web 服务就应该为 API 写测试。如果是 CLI 程序就针对 CLI 写测试。如果是编译器、解释器等则基本上所有测试都应是示例程序附带少量元数据如这一行应该报错或应该能构建并运行且产生如下输出。在 Zulip 中的具体落地Zulip 的 Web 应用、移动客户端和第三方 API 客户端使用同一个 API因此服务端测试大多是针对 Zulip API 写的。入站 webhook 的测试方式是把从真实第三方服务捕获的实际 payload发送到 webhook 端点再验证 webhook 是否产生了预期的 Zulip 消息输出——测试的正是真实接口本身。仓库中 zerver/webhooks/ 目录下大量*.json文件超过 1000 个就是这些真实捕获 payload 的固件。总结Zulip 的集成 vs 单元测试取舍尽管 Zulip 追求覆盖服务端每一条重要代码路径这通常与单元测试关联但大多数测试实质上是集成测试向 Zulip 服务器发送一个完整的 HTTP API 查询然后检查 HTTP 响应以及请求之后服务器的内部状态是否正确。遵循系统设计中的端到端原则end-to-end principle只要可能就写执行完整流程的测试例如注册一个新 Zulip 账号而不是逐个测试单个函数的实现。为此Zulip 投资于性能一方面是为了给用户好的体验另一方面同样重要的是让测试套件快到足以支撑这种测试写法。避免复制具有安全影响的代码开发出安全漏洞很少的软件极其困难。Zulip 避免安全逻辑 bug 的重要策略是为所有处理不受信任用户输入的代码设计出无需写和评审无穷无尽的测试、也不要求每个开发者都擅长思考安全边界情况的模式。具体做法是编写少量精心设计的函数如access_stream_by_id对它们进行仔细测试然后用 lint 及其他编码约定强制要求所有可能把数据分享给用户的代码路径对数据的访问都必须经由这些函数。这样与其让每个视图函数各自实现用户能否访问某频道的安全检查、再逐个测试这些复制出来的逻辑不如对每种主要数据结构、每种访问级别只做一次这项工作。access_by函数的设计风格这些access_*_by_*函数有专门的编写风格每个条件判断独占一行以便测试覆盖率工具帮助验证每个分支都被测到有详细的注释精心设计错误处理避免泄露所请求的频道 ID 是否存在之类的信息。以 zerver/lib/streams.py 的access_stream_by_id为例可以看到这种风格的完整实现它先get_stream_by_id_in_realm取频道取不到时抛出统一的_(Invalid channel ID)错误随后进入access_stream_commonzerver/lib/streams.py。access_stream_common的 docstring 明确写道一个设计目标是对无法访问的频道与不存在的频道返回的错误信息相同并逐一检查跨 realm 访问AssertionError防御、订阅关系、频道停用状态、基础访问权限check_basic_stream_access含require_active_channel与require_content_access两个可选开关。频道 ID 是int类型users.py中还有对应的access_user_by_id等函数覆盖用户数据访问。Zulip 也会为给定视图写非法访问时给出恰当错误的测试但这些测试属于纵深防御防止非法访问频道的主要手段是不提供让服务端代码绕过安全校验函数直接拿到Stream对象的途径。共享测试设置代码为权限检查或错误处理代码写测试非常常见。此时最好的做法是在成功测试与失败测试之间共享测试设置代码。例如当测试一个返回布尔值的函数而不是抛出带特定错误消息的异常时通常更适合写一个测试函数test_foo让它多次调用被测函数并针对各个测试条件验证输出。这样做的好处是能保证测试设置只在有意为之的地方不同。做得好时可以避免那种极其常见的失败模式——test_foo_failure测试因错误的原因而通过例如操作失败不是因为权限检查而是因为某个必需的 HTTP 参数只被加到了相邻的test_foo_success里。换句话说成功用例与失败用例共享同一套 setup意味着两者之间的唯一差异就是你要验证的那个条件从而让每个测试都真正测到了它声称要测的东西。没有测试过的东西大概率是坏的即使是最优秀的程序员也会不断犯错。而且如果大规模重构对保持代码库可读、愉快、正确非常重要会带来很高的制造隐蔽 bug的风险那么重构就无法进行。因此每个改动都需要测试。对于业务逻辑最佳选择通常是高质量的自动化测试——设计成对未来重构健壮。但有些事情如文档和 CSS只能通过在浏览器里查看元素并尝试那些可能不工作的东西来测试。测什么取决于什么容易坏例如对 Zulip 的 Markdown 文档做重大改动后如果你没有在视觉上验证每一种特殊格式、没有点击过每一个新链接那么你很可能会引入 bug。仓库中的 tools/check-templates、zerver/tests/test_templates.py模板渲染与浅层测试的排除列表以及tools/test-backend运行结束时对没有任何测试渲染过的模板的报错检查正是对这一原则的工程化落实。人工测试不仅能抓住 bug还能帮助开发者更了解系统、思考他们正在做的功能的既有语义。因此当提交影响 UI 的 Pull Request 时展示一段功能工作时的录屏screencast非常有帮助它能让评审者省下本会用于手动测试你改动的时间。把测试哲学应用到自己的项目中从 Zulip 的实践中可以提炼出几条可直接迁移的行动准则把测试速度当作一等公民为全量后端套件、全量前端套件设定明确的耗时预算Zulip 分别是 1 分钟与 10 秒任何让套件变慢的改动都要被严肃对待。隔离一切外部依赖让测试离线可跑外发请求一律 mock为数据库/缓存设置隔离机制事务回滚、随机 key 前缀、模板数据库副本。面向稳定的接口写测试而不是面向内部实现优先测试端到端 API、CLI、真实 webhook payload而不是被发明出来、随时可能变化的内部函数。把安全逻辑收敛为少量精心设计的函数用风格约定强制所有访问路径都经过它们让测一次、处处生效。共享成功与失败用例的设置代码防止失败测试因错误原因通过。承认没有测试过的东西大概率是坏的能自动化的业务逻辑用高质量自动化测试文档、CSS 等只能靠人工验证的就把人工验证清单视觉检查、逐个点击链接纳入改动的完成标准。这些原则共同解释了为什么 Zulip 能在保持高贡献者流动性的同时维持代码质量——正如 docs/testing/philosophy.md 所说自动化测试正是让项目能够持续取得进展的巨大部分而本文所引用的 tools/test-backend、tools/test-js-with-node、zerver/lib/test_runner.py、zerver/lib/streams.py 等源码就是这套哲学在工程实践中的具体注脚。【免费下载链接】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),仅供参考