Sentry 开发者贡献指南 - 测试技巧

发布于 1970年 01月 01日 08:00

作为 CI 流程的一部分,我们在 Sentry 运行了多种测试。
本节旨在记录一些 sentry 特定的帮助程序,
并提供有关在构建新功能时应考虑包括哪些类型的测试的指南。

获取设置

验收和 python 测试需要一组有效的 devservices
建议使用 devservices 来确保所需要的服务正在运行。
如果您还使用本地环境进行本地测试,您将需要使用 --project 标志将本地测试卷与测试套件卷分开:

# 关闭本地测试服务。
sentry devservices down

# 打开带有 test 前缀的服务以使用单独的容器和卷
sentry devservices up --project test

# 验证测试容器是否正确出现
docker ps --format '{{.Names}}'

# 稍后当您完成运行测试并想再次运行本地服务器时
sentry devservices down --project test && sentry devservices up

使用 --project 选项时,您可以确认哪些容器正在运行 docker ps
每个正在运行的容器都应该以 test_ 为前缀。
有关管理服务的更多信息,请参阅 devservices docs 部分。

Python 测试

对于 python 测试,我们使用 pytestDjango 提供的测试工具。
在此基础之上,我们添加了一些基本测试用例(在 sentry.testutils.cases 中)。

端点集成测试是我们大部分测试套件的重点所在。
这些测试帮助我们确保我们的 customersintegrations 和前端应用程序的 API 继续以预期的方式工作。
您应该努力包含涵盖各种用户角色、跨组织/团队访问场景以及无效数据场景的测试,因为这些在手动测试时经常被忽略。

运行 pytest

您可以根据更改的范围使用 pytest 运行单个目录单个文件单个测试

# 对整个目录运行测试
pytest tests/sentry/api/endpoints/

# 对目录中匹配模式的所有文件运行测试
pytest tests/sentry/api/endpoints/test_organization_*.py

# 从单个文件运行测试
pytest tests/sentry/api/endpoints/test_organization_group_index.py

# 运行单个测试
pytest tests/snuba/api/endpoints/test_organization_events_distribution.py::OrganizationEventsDistributionEndpointTest::test_this_thing

# 在匹配子字符串的文件中运行所有测试
pytest tests/snuba/api/endpoints/test_organization_events_distribution.py -k method_name

pytest 的一些常用选项是:

  • -k 通过子字符串过滤测试方法/类
  • -s 在运行测试时不要捕获标准输出。

有关更多使用选项,请参阅 pytest 文档。

在测试中创建数据

Sentry 还添加了一套 factory 辅助方法,可帮助您构建数据以针对其编写测试。
sentry.testutils.factories 中的工厂方法可用于我们所有的测试套件类。
使用这些方法来建立所需的组织项目和其他基于 postgres 的状态。

您还应该使用 store_event() 以类似于应用程序在生产中所做的方式存储事件。
存储事件需要您的测试继承自 SnubaTestCase
使用 store_event() 时,请注意在事件上设置过去timestamp
省略时,timestamp 将使用 'now',这可能会导致由于 timestamp 边界而无法选择事件。

from sentry.testutils.helpers.datetime import before_now
from sentry.utils.samples import load_data

def test_query(self):
    data = load_data("python", timestamp=before_now(minutes=1))
    event = self.store_event(data, project_id=self.project.id)

设置选项和功能标志

如果您的测试是针对带有功能标记的端点,或者需要设置特定选项。
您可以使用辅助方法将配置数据更改为正确的状态:

def test_success(self):
    with self.feature('organization:new-thing'):
        with self.options({'option': 'value'}):
            # Run test logic with features and options set.

    # Disable the new-thing feature.
    with self.feature({'organization:new-thing': False}):
        # Run you logic with a feature off.

外部服务

使用 responses 库为您的代码发出的出站 API 请求添加存根响应。
这将帮助您相对轻松地模拟成功和失败的场景。

可靠地使用时间

在编写与摄取事件相关的测试时,我们必须在事件的约束内操作不能超过 30 天。
因为所有事件都必须是最近的,所以我们不能使用传统的时间冻结策略在测试中获得一致的数据。
我们不是选择任意的时间点,而是从现在开始向后工作,并且有一些辅助函数可以这样做:

from sentry.testutils.helpers.datetime import before_now, iso_format

five_min_ago = before_now(minutes=5)
iso_timestamp = iso_format(five_min_ago)

这些函数生成 datetime 对象,以及相对于当前的 ISO 8601 格式的 datetime 字符串,
使您能够在已知时间偏移处拥有事件,而不会违反 relay 强加的 30 天限制。

在测试中检查 SQL 查询

将以下内容添加到项目根目录中的 conftest.py 中:

import itertools
from django.conf import settings
from django.db import connection, connections, reset_queries
from django.template import Template, Context

@pytest.fixture(scope="function", autouse=True)
def log_sql():
    reset_queries()
    settings.DEBUG = True

    yield

    time = sum([float(q["time"]) for q in connection.queries])
    t = Template(
        "{% for sql in sqllog %}{{sql.sql|safe}}{% if not forloop.last %}\n\n{% endif %}{% endfor %}"
    )
    queries = list(itertools.chain.from_iterable([conn.queries for conn in connections.all()]))
    log = t.render(Context({"sqllog": queries, "count": len(queries), "time": time}))
    print(log)

现在,在测试期间执行的所有 SQL 都将打印到标准输出。
建议使用 pytest-k 选择器缩小输出范围。另请注意,您需要通过 -s 来查看标准输出。

验收测试

我们的验收测试利用 seleniumchromedriver 来模拟用户使用前端应用程序和整个后端堆栈。
我们在 Sentry 使用验收测试有两个目的:

  1. 涵盖仅通过端点测试或仅使用 Jest 无法涵盖的工作流程。
  2. 通过我们的视觉回归 GitHub Actions 为视觉回归测试准备快照。

验收测试可以在 tests/acceptance 中找到,并使用 pytest 在本地运行。

运行验收测试

当您运行验收测试时,webpack 将自动运行以构建静态资资源。
如果您在创建或修改验收测试时更改 Javascript 文件,
则每次更改后都需要 rm .webpack.meta 以触发静态资源的重建。

# 运行单个验收测试。
pytest tests/acceptance/test_organization_group_index.py \
    -k test_with_onboarding

# 运行带有头的浏览器,以便您可以观看它。
pytest tests/acceptance/test_organization_group_index.py \
    --no-headless=true \
    -k test_with_onboarding

# 打开每个 snapshot image
SENTRY_SCREENSHOT=1 VISUAL_SNAPSHOT_ENABLE=1 \
    pytest tests/acceptance/test_organization_group_index.py \
    -k test_with_onboarding

如果您看到:
WARNING: Failed to gather log types: Message: unknown > command: Cannot call non W3C standard command while in W3C mode
则表示 Webpack 未正确编译资源。

定位元素

因为我们使用 emotion,所以我们的类名通常对浏览器自动化没有用。
相反,我们自由地使用 data-test-id 属性来定义浏览器自动化和 Jest 测试的 hook 点。

处理异步动作

我们所有的数据都异步加载到前端,验收测试需要考虑各种延迟和响应时间。
我们倾向于使用 seleniumwait_until* 特性来轮询 DOM,直到元素出现或可见。
我们不使用 sleep()

视觉回归

像素很重要,因此我们使用视觉回归来帮助捕捉 Sentry 渲染方式的意外变化。
在验收测试期间,我们捕获屏幕截图并将您的拉取请求中的屏幕截图与批准的基线进行比较。

虽然我们对视觉回归有相当广泛的覆盖,但仍有一些重要的盲点:

  • 悬停(Hover)卡片与悬停状态
  • 模态窗口
  • 图表和数据可视化

所有这些组件和交互通常不包含在可视化快照中,您在处理其中任何一个时都应该小心。

处理不断变化的数据

因为视觉回归比较图像快照,而且我们数据的很大一部分处理时间序列数据,
所以我们经常需要用 'fixed' 数据替换基于时间的内容。
您可以使用 getDynamicText 帮助程序为依赖于当前时间或变化
过于频繁而无法包含在可视快照中的组件/数据提供固定内容。

Jest 测试

我们的 Jest 套件涵盖为前端组件提供功能和单元测试。
我们更喜欢编写与组件交互并观察结果(导航、API 调用)的功能测试,
而不是检查 prop 传递和 state 突变。
请参阅 Frontend Handbook 了解更多 Jest 测试技巧。

# Run jest in interactive mode
yarn test

# Run a single test
yarn test tests/js/spec/views/issueList/overview.spec.js

API Fixtures

因为我们的 Jest 测试在没有 API 的情况下运行,
所以我们有各种 fixture 构建器可用于帮助生成 API 响应有效负载。
TestStubs 全局包括 tests/js/sentry-test/fixtures/ 中的所有 fixture 函数。

您还应该使用 MockApiClient.addMockResponse() 来设置您的组件将进行的 API 调用的响应。未能模拟端点将导致测试失败。

CI 中的 Kafka 测试

Snuba 测试套件 (.github/workflows/snuba-integration-test.yml) 是唯一真正让 KafkaCI 中运行的测试套件。
如果您有一个需要 Kafka 运行的测试,那么这些测试需要嵌套在 Snuba 测试文件夹 (tests/snuba/) 下。
如果不这样做,您的测试将超时并在 GH actions 中被取消。

更多