尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PostHog 测试编写实战指南:从真实事故形态目录反推「值得写」的测试

PostHog 测试编写实战指南:从真实事故形态目录反推「值得写」的测试 PostHog 测试编写实战指南从真实事故形态目录反推「值得写」的测试【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthogPostHog 的测试技能文档writing-tests沉淀了一份独一无二的资产一份从合并的fix:/revert:PR 中逐条提取、并与生产事故交叉验证的bug 形态目录mistakes-we-make.md。本篇文章以这份目录为骨架结合仓库源码与测试用例系统讲解 PostHog 团队如何判断一个测试是否值得写、应该写在哪一层并逐类给出可复制的测试形态与真实事故案例帮助你为自己的改动快速定位最廉价的回归防线。读完本文你将掌握PostHog 反复踩坑的五大廉价测试可拦截的 bug 形态错误分类、空输入崩溃、跨租户泄漏、时区错误、HogQL 类型强转、四类单元测试救不了的真实故障、以及两套判断工具——测试金字塔和两问门禁。这份目录从哪来先有事故后有测试文档开头交代了目录的生成方法这是它与泛泛而谈的测试指南最大的区别来源是真实的合并记录逐条来自已合并的fix:和revert:PR且每个案例都在diff 层面核对过——这个 PR 是否真的补了回归测试、补在哪一层单元 / Django TestCase / ClickHouse / Playwright。与生产事故交叉验证不仅看 PR还对照重复出现的生产事故确保列出的都是真的发生过的失败模式。因此它的使用方式很明确用这份目录去瞄准一个我们真正撞过的失败模式而不是虚构的假设。找到你的改动命中的形态在标注的层级写测试如果改动对不上目录里的任何形态、又不是真正的新行为就要怀疑这个测试是否值得存在。文档开篇给出了两条头条结论是全篇的判断纲要大多数能用廉价测试拦截的 bug 是边界与契约错误——错误被错误分类、null/畸形输入、跨租户泄漏、非 UTC 时间——它们被拦截在金字塔底部。一些真实且代价高昂的失败模式无法用廉价单元测试覆盖查询性能、迁移时序、异步收尾挂起、重构回归、资源耗尽。为它们硬写单元测试只会带来虚假的安全感。动手写之前先分清你面对的是哪一种。这两条结论与 SKILL.md 中的测试金字塔一脉相承。金字塔的每一级大约比下一级慢/脆一个数量级纯函数 / 单元 → kea logic 测试 → Django TestCase → ClickHouse 支撑的测试 → Playwright e2e 最廉价 最昂贵追求的是比例而非上限底部铺满廉价测试顶部极少昂贵测试。想要更多覆盖在底部加。而当某个逻辑难以廉价测试时那是一个设计信号——应该抽取而不是升级测试层级。廉价测试就能拦截的 bug——放心写可重试错误 vs 不可重试错误的误分类数据仓库导入源把永久性失败认证失效、集成被删除、密码过期映射成可重试错误于是 Temporal 无限重试、刷爆错误追踪或者反过来一次瞬时抖动被判定为终止错误静默禁用了客户的同步。这是fix:PR 中最常见的簇也是测试收益最好的形态。在哪一层拦截纯单元测试直接断言某个源把给定的异常字符串映射到了正确的非可重试类别。不需要 Temporal、不需要 ClickHouse且双向都要断言——既断言该终止的确实终止也断言该重试的确实重试。PostHog 的真实案例PR #63677Salesforce已删除的 OAuth 集成抛出Integration not found而错误映射表没有匹配到这个字符串于是它被当成可重试错误无限重试。修复测试同时断言Integration not found是非可重试的且Read timed out仍然是可重试的。PR #63681SnowflakeSpecified password has expired不在非可重试集合里密码过期被无限重试。修复把它加入非可重试集合。PR #63798Postgres反向案例SSL connection has been closed unexpectedly被错误地标记为非可重试导致探测途中的一次掉线就永久禁用同步。修复保持其可重试性——这正是双向断言要防住的反向方向。源码佐证这套错误分类在仓库中确实是以字符串匹配异常类名的方式实现的。utils.py 中的handle_non_retryable_errors装饰器包裹批导出 activity捕获异常后检查e.__class__.__name__ in non_retryable_error_types命中则返回携带错误的BatchExportResult终止不重试未命中则重新抛出交给 Temporal 重试。这正是文档所说把某个异常字符串映射到正确的类别的底层实现也解释了为什么字符串写错/漏写一个类名就会产生目录里那些事故——代码注释里还留着一个 TODO未来应该用异常类而不是字符串。每个目标端都维护着自己的NON_RETRYABLE_ERROR_TYPES列表例如 postgres_batch_export.py 中逐条注明了原因NotNullViolation重试无益、UniqueViolation用户表上的唯一约束与批导出的重复数据冲突、StringDataRightTruncationVARCHAR 列太小等而对应的make_retryable_with_exponential_backoff(retryable_exceptions(psycopg.OperationalError, psycopg.errors.ConnectionTimeout))则明确把连接类瞬时错误留在可重试侧——与 PR #63798 的修复方向一致。Temporal 工作流里再通过RetryPolicy(non_retryable_error_types[...])见 batch_exports.py兜底。写这种测试时直接对着这份列表断言字符串映射是最快也最贴近事故的方式。Null / 空 / 畸形输入 → 崩溃代码假定某个值存在或形态正确数据库字段、JSON 请求体、group type结果在遇到坏值时直接崩溃而不是优雅降级。这类问题很常见相对其上线后的代价防护成本极其低廉。在哪一层拦截能触达边界的最廉价层级——用坏值直接做纯单元调用或用 DjangoTestCase打端点。不需要 ClickHouse。真实案例PR #62757序列化实验时处理 null 指标。一个NULL的metrics列流到了enumerate(None)导致一行坏数据就把整个实验列表打成 500。测试把 null/空组合参数化后分别打到列表端点和详情端点。PR #11792事件 properties 不是 JSON 时应返回 400 而不是 500。对非 dict 的properties做下标访问抛出了TypeError修复把它转成ValueError映射为 400而单元测试直接调用函数并断言这一行为——层级比端点测试还要低一格。PR #61076为wordPluralize防御 null 的group_type。null 的 group type 曾导致功能开关页面渲染崩溃测试同时写在两个地方helper 层Jest和产出该 null 的转换器层Python 单元测试。源码佐证前端工具函数 strings.ts 中的wordPluralize其 Jest 测试 strings.test.ts 正是文档描述的双层写法——常规词形company→companies、person→people、child→children之外专门断言了wordPluralize()返回空串、wordPluralize(null as unknown as string)也返回空串。注意这里的形态把坏值传进函数本身而不是绕过它这正是在边界层测试边界的范本。租户隔离 / 作用域IDOR某个查询或端点没有按请求方 team/org 限定作用域导致跨租户读泄漏甚至跨租户写。在哪一层拦截PostHog 标准的 IDOR 覆盖形态是一个 DjangoTestCase——创建两个 org断言 org B 既看不到也改不了 org A 的行。真实案例PR #61901补作用域检查。价值体现在那些具体的两 org 测试里例如test_plugin_unused_does_not_leak_other_orgs断言其他 org 的 plugin id 不在结果中以及 dashboard-collaborator 的跨项目配对测试断言 404、且权限行未被改动。文档特别提醒一个实操要点这个 PR 本身捆绑了若干无关修复所以引用时要引用具体测试而不是裸引用 PR——这既是检索经验也是评审经验。时区 / 非 UTC 正确性代码假定世界是 UTC 的日期分桶、分页游标、图表标签在非 UTC 团队面前整体偏移一天——而 CI 里默认的TZUTC恰好掩盖了它。在哪一层拦截通常是金字塔最廉价的一级——一个参数化多个非 UTC 时区外加一个 DST 边界的纯单元测试。但注意例外当时区截断发生在ClickHouse 内部时廉价测试看不见它必须真实执行 SQL。真实案例一廉价、一昂贵恰好成对PR #57593廉价范例时区安全的日期解析。前端解析读取了系统墙钟导致 UTC 以东的地区日历回滚一天。一个覆盖 UTC/LA/Tokyo/Berlin 加 DST 边界的纯 Jest 测试就抓住了它——文档称之为干净的廉价范例。PR #61111不廉价time_bucket截断必须钉在 UTC 上。toStartOfDay在会话时区网格上截断而游标按 UTC 打印导致非 UTCsession_timezone下 keyset 第二页变空。回归测试要插入行并用session_timezoneUS/Pacific跑真实 SQLClickHouse 支撑的TestCase。文档的忠告是分清你面对的是哪一种——先判断截断发生在哪一侧再决定测试层级别把廉价假设套在不廉价的问题上。HogQL 打印器 / 类型强转HogQL 打印器printer产出的 SQL 让 ClickHouse 因 cast 错误崩溃或返回错误结果。这类问题有一个经典陷阱两种测试层级被混为一谈。SQL 形状变化→ 用打印 SQL 断言或.ambr快照test_printer.py不执行。快但它只证明 SQL 变了不证明 ClickHouse 不再报错。错误结果 / 硬错误→ 用 ClickHouse 支撑的TestCase真实执行查询。真实案例PR #63220让toBool变成 null-safe。裸toBool对 UUID 形状的字符串会硬失败修复改为accurateCastOrNull。测试用打印 SQL 的字符串断言加.ambr快照捕获文档冷静地指出快照只证明 cast 包裹存在仅此而已——它防的是形状回归不防执行错误。PR #58713在空值检查之前强转 funnel 聚合目标。数值类型的属性撞上 ClickHouse 错误码 72Cannot read floating point value。由于.ambrdiff 本身一个toString()包裹无法证明错误已消失真正的测试是对 ClickHouse 执行.calculate()。源码佐证类型强转的落点在 base.py 的visit_type_cast——bool/boolean分支输出accurateCastOrNull(expr, Bool)int走toInt64、text走toString。而 test_printer.py 中的参数化断言(toBool, toBool(uuid), accurateCastOrNull(events.uuid, ...))正是文档所说的打印 SQL 字符串断言形态注释里也写明它们都经由 accurateCastOrNull因此不可解析的值会变成 NULL 而不是抛错——形状测试与结果测试各司其职这就是这条目录的核心教训。单元测试救不了的真实昂贵 bug——别用错工具这些失败模式真实且代价高但廉价单元测试要么抓不住、要么给出虚假的安全感。应该换用正确的工具而不是写一个证明不了任何东西的测试。非索引友好 / 无界查询一个函数把索引列包进表达式toString(uuid) IN ...或让team_id未被绑定导致查询退化为全表扫描在大团队上超时。正确的防线是assertNumQueries/ 查询次数上界或人工审查打印出来的谓词——不是 happy-path 测试。文档在这里有一段难得的诚实自省PostHog 自己历史上这类问题大多是手工验证而非自动化上界拦截的——PR #61864未绑定的team_id→ 扫描只用 mock 钉住了一个 RPC 名称PR #62417索引列被包进toString断言的是输入校验而不是查询形状。这个缺口恰恰就是查询上界测试应该补上的地方。迁移 / 应用时序代码在迁移创建表之前就去读它。PR #59873 就是 ClickHouse 迁移在并行迁移下读取了尚不存在的posthog_instancesetting表。没有任何入库测试能守住这个真正的安全网是 CI 里的迁移回放migration replay它专门验证应用顺序。文档直言不要写一个假装能守住的单元测试。异步 / Temporal 收尾挂起fixture 收尾里的一个不可取消的sync_to_asyncgRPC 调用会挂起整个 job——PR #62339 的修复方式是移除该调用、依赖数据库 CASCADE而不是加测试。文档还点出一个相邻陷阱一个 mock 掉了自己本该验证的边界的测试等于什么都没抓。PR #60302 修复了一个真实的工作流日志器崩溃但它的 logger 测试 mock 了 logger——于是无论崩溃是否存在测试都会通过回归时它也无法发现。测试边界的原则是mock 真正的边界网络、外部 API、时钟、队列不要 mock 自己的内部实现否则测试就退化成 SKILL.md 里点名的变更检测器测试。逃过正确性测试的重构回归行为保持的重构在一个测试没覆盖的轴上回归。这里的教训不是补一个特征化测试而是**覆盖真正发生变化的那个行为**。PR #59920 即便存在会执行数据的正确性测试仍被 revert——回归的行为恰好在该测试断言的覆盖之外PR #56785 的 helper 测试和 API 测试在重构与 revert 两个方向上都干净通过。它们证明了什么证明了测试断言的是不随行为变化的东西。根本不是「测试形」的问题最后这一类主导了事故数量但它们的防线是监控、容量与告警——永远不是单元测试。负载下的资源耗尽OOM、连接池枯竭、事件循环阻塞。与负载相关预防靠容量规划和告警。一个已知的意外 O(n) 可以写定向性能测试但负载本身无法用测试覆盖。基础设施 / 第三方故障节点崩溃、DNS 或网络变更、磁盘耗尽、上游供应商宕机。根因在应用层之下对策是 runbook 和监控。实操把目录变成你的测试决策流程配合 SKILL.md 的门禁这份目录可以落地成三步两问门禁写任何测试前回答两句——这个测试拦截的、且现有测试都没拦截的真实回归是什么回答不出具体 bug、路径和会触发的输入就不要写为什么它不能作为现有最近测试的一个参数化 caseparameterized/test.each新测试函数是扩展失败时的最后手段。对照目录选层级你的改动命中上面哪条形态错误分类→纯单元双向断言空输入→边界层直接传坏值IDOR→两 org 的 DjangoTestCase时区→注意截断发生在进程内还是 ClickHouse 内HogQL→先分清是形状测试还是结果测试。对不上目录、又不是新行为——质疑测试的价值。PR 描述里留一句自证PR 模板的 How did you test this code? 处写一行即可例如为test_cohort_query新增空/单元素/超大 cohort 三个 case——守护刚修掉的 500无法扩展现有测试因为没有测试覆盖空路径。写不出这句话就说明这个测试不该进 PR。五个不写同样值得牢记不测框架行为、不写变更检测器测试、不用十个重复测试代替一个参数化测试、不为覆盖率数字追分支、绝不做跨语言源码爬取Python 测试去 grep.ts文件之类的脆字符串耦合——两棵树的共识应该落在生成的产物或共享数据文件上。最后回到那份目录的定位它是 PostHog 用fix:/revert:PR 和线上事故反向工程出来的失败形态档案。它的价值不在某个具体测试而在于逼你先回答我在防哪个真实事故再决定写不写、写在哪一层。如果你对不上目录先怀疑的不是目录而是那个测试。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表