Sentry 仓库 Python 测试指南:测试目录组织、工厂方法、EAP 时间窗口与备份覆盖规范
2026/9/10 4:17:00 网站建设 项目流程

Sentry 仓库 Python 测试指南:测试目录组织、工厂方法、EAP 时间窗口与备份覆盖规范

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

本文以 tests/AGENTS.md(由 tests/CLAUDE.md 通过@AGENTS.md引用的 Python Testing Guide)为主体,结合 Sentry 仓库中的testutils、flake8 插件与tests/snuba等源码实现,系统讲解 Sentry 后端 Python 测试的编写规范。读者将掌握:新测试用例应放在哪里、如何用工厂方法替代直接建表、如何规避日期漂移与 EAP 降采样陷阱、以及备份/迁移模型测试的强制覆盖要求。

一、引言:Sentry 测试规范的核心脉络

Sentry 是一个 developer-first 的错误跟踪与性能监控平台,其后端测试体量庞大(tests/目录下有 2800 个 Python 测试文件与 1482 个 pysnap 快照)。为了保证测试在本地、Snuba CI 与 Sentry CI 中行为一致,仓库以AGENTS.md体系沉淀了一套强制的 Python 测试规范。本文面向两种读者:一是给 Sentry 提交代码的开发者,需要知道"测试写在哪、怎么写、会被什么 lint 规则拦住";二是想借鉴大型 Python 项目测试工程实践的工程师,Sentry 在测试文件定位、工厂方法、时间稳定性(date-stable tests)、EAP 数据集窗口、备份/迁移模型覆盖等方面给出了可复制的方案。

规范的总入口是 tests/CLAUDE.md,它只有一行@AGENTS.md,指向 tests/AGENTS.md——这才是完整的 Python Testing Guide。仓库根目录的 AGENTS.md 同时给出了命令执行指引:所有 Python 命令必须在 virtualenv 中运行(如.venv/bin/pytest ...),测试命令参见其 "Command Execution Guide" 一节;而前端 React/TypeScript 测试(*.spec.tsx、RTL、MockApiClient)则走独立的react-testingskill,不在本文范围。

二、新测试用例放哪里:tests/ 镜像 src/ 的定位规则

规范首先解决"测试文件该放哪"的问题,规则非常机械且严格:

  • 代码位置src/sentry/foo/bar.py→ 测试位置tests/sentry/foo/test_bar.py
  • 做法是把tests/前缀拼到源码路径前,再把模块名加上test_前缀

即"给路径加tests/、给模块名加test_"。这一镜像结构在 tests/AGENTS.md 的 File Location Map 一节被再次强调:Python 测试的tests/目录镜像src/目录结构,fixtures 放在fixtures/{type}/,工厂类集中在tests/sentry/testutils/factories.py

特例(强约束):保证 Snuba 兼容性的测试必须放在tests/snuba/,因为该目录下的测试还会在 Snuba 的 CI 中运行。这保证了 Sentry 前端调用 Snuba 的契约在两边都被验证。

关于"必须加到已有测试文件而非新建":规范要求修复 bug 或新增功能时,把测试用例加到已有的测试文件中,而不是新建文件。这一做法保持了测试文件的收敛性,也让tests/src/的镜像结构始终可预测。

三、测试基类与标准模式:APITestCase 示例

规范给出一个标准测试模式示例,位于tests/sentry/core/endpoints/test_organization_details.py

from sentry.testutils.cases import APITestCase class OrganizationDetailsTest(APITestCase): endpoint = "sentry-api-0-organization-details" def test_get_organization(self): org = self.create_organization(owner=self.user) self.login_as(self.user) response = self.get_success_response(org.slug) assert response.data["id"] == str(org.id)

从源码看,cases.py 聚合了 Sentry 测试所需的一切:APITestCase(继承自rest_framework.test.APITestCase)、before_now时间助手、EAPClientload_data(从sentry.utils.samples加载事件样本)等;self.create_organization实际由 factories.py 中的Factories.create_organization提供。

两条硬性注解

  1. 测试必须是**纯过程式(procedural)**的,禁止分支逻辑——后端测试中几乎永远不需要if语句。如果发现测试里需要条件分支,通常是测试设计有问题的信号。
  2. self.get_success_response(org.slug)这类断言型请求助手会在响应非 2xx 时直接失败,天然保证了"测试即断言"。

四、时间稳定性:禁止把当前/未来年份硬编码进测试"现在"

S015 规则是 Sentry 测试工程里最容易踩的坑之一,其动机与 Snuba 的保留期(retention)机制强相关:

不要在模块级或类级作用域(或freeze_time(datetime(...))中)把当前或未来 UTC 日历年份硬编码为测试的"现在"。那会随时间漂移进 Snuba 的保留期之外。

正确做法:

  • before_now(...)(或now - timedelta)构造相对时间;
  • 刻意构造历史 fixture 时使用较早的固定年份
  • 函数体内的固定时间戳(fixtures、断言)是允许的。

before_now定义在src/sentry/testutils/helpers/datetime.py,并被 cases.py 大量导入使用(例如测试中用before_now(minutes=9)造数据)。

lint 侧的实现:这条规则由 tools/flake8_plugin.py 以S015编码实现(源码第 110 行起的注释与_s015_msg表明:S015 会标记"在模块/类作用域或freeze_time(...)中使用大于等于当前 UTC 年份的字面量")。该插件还实现了 S001~S024 一系列 Sentry 专属 lint 规则(如 S004 禁止assertRaises、S014 禁止直接用unittest.mock等),通过prek.venv/bin/prek run -q)统一执行。这也是"时间稳定测试"能够被机器强制而非仅靠代码评审的原因。

五、EAP / Snuba 端点测试:30 天保留期与降采样陷阱

这是本指南技术含量最高的一节,直接关系到测试能否稳定通过。

5.1 问题背景:两套默认窗口的冲突

  • Snuba 的 EAP(Events/Attributes/Profiling)outcomes 路由把标准保留期默认设为 30 天,当查询起点早于该窗口时强制走 tier 8(降采样/下采样存储)。
  • 而 Sentry API 对未显式指定的窗口默认是90 天
  • 于是,Snuba 集成测试若省略显式窗口,就会"少算"近期数据;且epm()/eps()/tpm()这类速率函数会除以错误的窗口,导致断言全错。

5.2 共享默认值:EAPClient 与 30d 注入

  • EAPClient/EAP_DEFAULT_STATS_PERIOD(eap.py)会给 EAP 数据集和已知 EAP 路径注入statsPeriod = EAP_FULL_FIDELITY_QUERY_DAYS(即30d)。
  • 继承OrganizationEventsEndpointTestBase的测试套件,还会通过client_get()/do_request()默认落到同一个30d窗口——这覆盖了 EAPClient 启发式规则漏掉的路径(例如某些 trace-meta 路由)。

从源码看,常量定义在 constants.py:EAP_FULL_FIDELITY_RETENTION_DAYS = 30EAP_FULL_FIDELITY_QUERY_DAYS与之相等,注释明确写着"这是仍能命中 tier 1 的最宽窗口,Snuba 会对起点早于 31 天的查询做降采样";而FULL_RETENTION_ITEM_TYPES(uptime results 与 preprod)是从不降采样的类型。EAPClient的实现(eap.py)展示了注入逻辑的细节:

  • 只有查询中没有任何窗口键(statsPeriodstatsPeriodStart/Endstartendrangetimestamp)时才注入;
  • 通过dataset/itemType/data_source判断是否 EAP 数据集,对完整保留数据集(如preprodSize)不注入;
  • 对路径片段/trace-items//ai-conversations//spans/fields//traces/做路径级兜底;
  • GET 请求会原样保留 query string,避免把#重编码成%23

5.3 对这些套件的硬性要求

  1. 优先使用client_get()/do_request()(或调用它们的本地助手),不要裸调self.client.get(...)——flake8S020会直接标记。S020 的实现同样在 tools/flake8_plugin.py:它针对继承OrganizationEventsEndpointTestBaseOrganizationEventsTraceEndpointBase的类(且路径命中tests/snuba/api/endpoints/test_organization_*),强制走会注入默认statsPeriod的助手。真实用例见 test_organization_events.py:client_get通过with_default_stats_period在未显式传窗口时补上statsPeriod = EAP_DEFAULT_STATS_PERIODdo_request则负责登录 + feature 开关 + 请求。
  2. 查询窗口保持 ≤30d,除非测试有意覆盖长期保留/降采样行为。
  3. 速率断言(epm/eps/tpm/…)必须使用真实请求窗口(通常是EAP_FULL_FIDELITY_QUERY_DAYS),不能拿更短的硬编码周期。
  4. 必须查询 >30d,要同时设置窗口并且:要么传standard_retention_days(最多 90),要么显式断言 tier-8 / 降采样行为。不得为了 CI 变绿而削弱断言

六、用工厂方法替代直接Model.objects.create

Sentry 测试的另一个硬约束是禁止直接调用Model.objects.create,必须按优先级使用工厂:

  1. Fixture 方法(如self.create_model),来自sentry.testutils.fixtures.Fixtures等基类;
  2. 工厂方法sentry.testutils.factories.Factories),当 fixture 不可用时使用。

规范给出的 diff 示例展示了正确做法:

- direct_project = Project.objects.create( - organization=self.organization, - name="Directly Created", - slug="directly-created" - ) + direct_project = self.create_project( + organization=self.organization, + name="Directly Created", + slug="directly-created" # Note: Ensure factory args match + )

理由:直接建表绕过了共享的测试设置逻辑。从源码看,factories.py 提供了一整套create_organization(第 512 行起)、create_project(第 694 行起)等方法,内部会补全创建组织/项目所需的一连串附属记录(API key、规则、书签等);fixtures 层(Fixtures)再把这些工厂方法包装成self.create_*测试助手。这也与"pytest而不是unittest"的规范相互呼应:pytest.raises取代assertRaises,减少样板代码,并复用工厂中定义的共享设置逻辑:

- self.assertRaises(ValueError, EffectiveGrantStatus.from_cache, None) + with pytest.raises(ValueError): + EffectiveGrantStatus.from_cache(None)

(顺带一提,assertRaises本身也会被 flake8 插件以S004规则拦截。)

七、备份/迁移(Backup/Relocation)测试的模型覆盖

凡是__relocation_scope__不等于RelocationScope.Excluded的模型,tests/sentry/backup/下的备份测试套件都会自动检查;新增这类模型(或给已有模型加字段)而不更新以下内容,CI 就会失败:

  1. 穷举 fixtures(backups.py):在对应的create_exhaustive_*方法中至少创建一个该模型的实例——org/project 作用域的模型放create_exhaustive_organization(源码第 440 行起),user 作用域的放create_exhaustive_user(第 379 行起)。否则tests/sentry/backup/test_exhaustive.py以及test_exports.py/test_imports.py中的ScopingTests会报 "Someexpected_modelsentries were not found" 或 "models were not included in the export"。
  2. 比较器(Comparators):如果模型有导入时会变化的字段(如DefaultFieldsModeldate_added/date_updated),要在get_default_comparators()(comparators.py)中注册DateUpdatedComparator("date_updated", "date_added")(该类定义于同文件第 188 行)。否则test_exhaustive_dirty_pks会因这些字段的UnequalJSON差异而失败。
  3. 覆盖检查tests/sentry/backup/test_coverage.py)可能还会要求:
    • 若模型有不基于 Organization/Global 作用域外键的唯一约束,需在test_imports.py中加碰撞测试(COLLISION_TESTED);
    • __relocation_scope__是一组作用域(set),需在test_models.py中加动态迁移作用域测试(DYNAMIC_RELOCATION_SCOPE_TESTED)。

导入/导出/diff 循环与比较器如何运作,详见 tests/sentry/backup/README.md。这套机制的本质是:任何可被备份导出的模型都必须证明自己能被完整导出、导入且 diff 干净,从而保证组织级数据迁移(relocation)的可靠性。

八、测试生态补充:Kafka/Arroyo 与 File Location Map

8.1 Kafka/Arroyo 组件的测试方式

规范在 "Testing Best Practices" 中给出:对 Kafka/Arroyo 组件,使用LocalProducer+MemoryMessageStorage而非 mock。这与 Sentry 大量依赖消息队列(事件流、任务 broker 等)的架构一致——用真实的内存存储验证消息的生产/消费语义,比 mock 更能捕获序列化与契约问题。

8.2 文件位置速查(File Location Map)

类型位置
Python 测试tests/镜像src/结构
Fixturesfixtures/{type}/
工厂类tests/sentry/testutils/factories.py

结合仓库根目录 AGENTS.md 的 "Context-Aware Loading" 一节:测试相关代码(tests/**/*.pysrc/**/tests/**/*.py)应遵循本文档(tests/AGENTS.md),后端src/**/*.py遵循 src/AGENTS.md,前端遵循 static/AGENTS.md。AGENTS.md是 AI Agent 指令的权威来源——新增或修改 agent 指引时,应更新对应 AGENTS.md,而不是写进编辑器规则文件。

九、本地执行与检查清单

根目录 AGENTS.md 给出了配套的执行方式(必须在 virtualenv 中运行):

# 环境准备(SENTRY_DEVENV_FRONTEND_ONLY=1 跳过迁移,仅测试足够) SENTRY_DEVENV_FRONTEND_ONLY=1 devenv sync direnv allow devservices up # 运行单个测试文件(不要裸跑 pytest,会非常慢) .venv/bin/pytest -n3 -svv --reuse-db tests/sentry/api/test_base.py # 提交前 lint(自动检测改动文件) .venv/bin/prek run -q

写测试时对照以下清单自查:

  1. 测试文件是否放在tests/sentry/<module>/test_<name>.py(Snuba 兼容性测试放tests/snuba/)?
  2. 是否添加到已有测试文件而非新建?
  3. 是否纯过程式、无分支逻辑?
  4. 是否用before_now等相对时间,避免 S015 拦截当前/未来年份?
  5. EAP 套件是否用client_get()/do_request()(避免 S020),窗口是否 ≤30d,速率断言是否用真实窗口?
  6. 是否用 fixture/工厂方法而非Model.objects.create?是否用pytest而非unittest
  7. 新增 relocation-scope 模型时,是否同步更新穷举 fixtures、比较器及碰撞/动态作用域测试?

十、总结

Sentry 的 Python 测试规范可以用四句话概括:位置镜像tests/src/,Snuba 特例另置)、时间稳定(相对时间优先,S015 机器强制)、窗口对齐(EAP 查询锁定 30d tier-1,S020 强制走注入助手)、覆盖闭环(模型备份测试由穷举 fixtures + comparators + 碰撞测试自动校验)。这套规范不是孤立的约定,而是与 tools/flake8_plugin.py 的 S 系列 lint、testutils 的共享基建以及 Snuba 的保留期机制深度咬合——理解底层动机,才能写出既通过 CI、又真实反映生产行为的测试。

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询