使用 Cypress 进行端到端测试
Metabase 使用 Cypress 进行“端到端测试”,即针对整个应用程序(包括前端、后端和应用程序数据库)执行的测试。这些测试本质上是用 JavaScript 编写的脚本,在 Web 浏览器中运行:访问不同的 URL,点击各种 UI 元素,输入文本,并断言事情按预期发生(例如,元素出现在屏幕上,或发生网络请求)。
在继续之前,请熟悉 Cypress 最佳实践。
入门
Metabase 的 Cypress 测试位于 e2e/test/scenarios
源代码树中,其结构大致反映了 Metabase 的 URL 结构。例如,admin “datamodel” 页面的测试位于 e2e/test/scenarios/admin/datamodel
。
我们的自定义 Cypress 运行器构建了自己的后端并创建一个临时 H2 应用程序数据库。当此进程被终止时,两者都会被销毁。本地主机的默认保留端口是 4000
。您可以同时在 localhost:3000
上运行本地 Metabase 实例。这甚至可能有助于调试。
标准开发流程
-
持续构建前端
a. 如果只需要前端,请运行
yarn build-hot
b. 如果您想在 Cypress 旁边运行本地 Metabase 实例,最简单的方法是使用
yarn dev
或yarn dev-ee
(两者都依赖于前端热重载) -
在单独的终端会话中(不终止前一个会话)运行
yarn test-cypress
。这将打开一个 Cypress GUI,让您选择要运行的测试。另外,请查看run_cypress_local.js
和e2e/test/scenarios/docker-compose.yml
以获取所有可能的选项。
运行选项
在终端中以无头模式运行所有 Cypress 测试
OPEN_UI=false yarn run test-cypress
您可以使用官方的 --spec
标志快速测试单个文件。此标志可用于运行文件夹中的所有 spec,或运行多个分类的 spec。有关说明,请参阅官方文档。
OPEN_UI=false yarn test-cypress --spec e2e/test/scenarios/question/new.cy.spec.js
您可以使用 --browser
标志指定运行 Cypress 测试的浏览器。有关更多详细信息,请参阅官方文档。
在 运行 模式下运行 Cypress 时,指定浏览器最有意义。另一方面,Cypress 打开 模式(GUI)允许轻松在系统上所有可用浏览器之间切换。然而,有些人即使在这种情况下也更喜欢指定浏览器。如果您这样做,请记住您只是为 Cypress 预选了一个初始浏览器,但您仍然可以选择不同的浏览器。
测试结构
Cypress 测试文件结构类似于 Mocha 测试,其中 describe
块用于分组相关测试,it
块是测试本身。
describe("homepage", () => {
it("should load the homepage and...", () => {
cy.visit("/metabase/url");
// ...
});
});
我们强烈建议使用来自 @testing-library/cypress
的 cy.findByText()
和 cy.findByLabelText()
等选择器,因为它们鼓励编写不依赖于 CSS 类名等实现细节的测试。
尽量避免重复测试应用程序的各个部分。例如,如果您想测试查询构建器的一些东西,请使用 openOrdersTable()
等辅助函数直接跳转到那里,而不是从主页开始,点击“新建”,然后“问题”等。
Cypress 文档
- 简介:https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html
- 命令:https://docs.cypress.io/api/api/table-of-contents.html
- 断言:https://docs.cypress.io/guides/references/assertions.html
提示/注意事项
contains
vs find
vs get
Cypress 有一套用于选择元素的类似命令。以下是一些使用它们的提示
contains
(默认情况下)对 DOM 中的文本 大小写敏感。如果它不匹配您预期的文本,请检查 CSS 是否已更新了大小写。您可以使用以下选项{ matchCase: false }
明确指示它忽略大小写。contains
匹配子字符串。给定两个字符串“filter by”和“Add a filter”,cy.contains(“filter”);
将匹配两者。为了避免这些问题,您可以传入一个固定字符串开头/结尾的正则表达式,或者将字符串范围限定到特定选择器:cy.contains(selector, content);
。
find
允许您在之前的选择中搜索。get
将搜索整个页面,即使是链式调用,除非您明确调整withinSubject
选项。
如何访问示例数据库表和字段 ID?
我们在 E2E 测试中使用的示例数据库随时可能更改,随之更改的还有对其表和字段的引用。切勿**硬编码这些 ID 的数字引用。我们提供了一个有用的机制来实现这一点,保证产生正确的结果。每次启动 Cypress 时,它都会获取有关示例数据库的信息,提取表和字段 ID,并将其写入 e2e/support/cypress_sample_database
JSON 文件,然后我们将其重新导出并提供给所有测试。
// Don't
const query = {
"source-table": 1,
aggregation: [["count"]],
breakout: [["field", 7, null]],
};
// Do this instead
import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
const query = {
"source-table": PRODUCTS_ID,
aggregation: [["count"]],
breakout: [["field", PRODUCTS.CATEGORY, null]],
};
增加视口大小以避免滚动
有时 Metabase 视图对于 Cypress 的默认 1280x800 视口来说有点大。这可能需要您滚动才能使测试正常工作。例如,虚拟化表甚至不会渲染视口之外的内容。为了避免这些问题,请增加特定测试的视口大小。除非您专门测试应用程序在窗口大小调整时的行为,否则请避免在测试中间使用 cy.viewport(width, height);
。而是使用可选的 Cypress 测试配置来设置视口宽度/高度。此配置适用于 describe
和 it
块。
describe("foo", { viewportWidth: 1400 }, () => {});
it("bar", { viewportWidth: 1600, viewportHeight: 1200 }, () => {});
代码重载 vs 测试重载
当您编辑 Cypress 测试文件时,测试将刷新并再次运行。但是,当您编辑代码文件时,Cypress 不会检测到该更改。如果您正在运行 yarn build-hot
,代码将在 Cypress 中重新构建并更新。您必须在新代码加载后手动点击重新运行。
当“contains helper”打开时进行检查
Cypress 的一个强大功能是您可以在测试的每个步骤之后使用 Chrome 检查器。他们还提供了一个有用的辅助工具,可以测试 contains
和 get
调用。此辅助工具会创建新的 UI,这会阻止检查器定位正确的元素。如果您想在 Chrome 中检查 DOM,则应关闭此辅助工具。
Uberjar 中放置了错误的 HTML 模板
yarn build
和 yarn build-hot
都会覆盖一个 HTML 模板以引用正确的 JavaScript 文件。如果您在为 Cypress 测试构建 Uberjar 之前运行 yarn build
,即使您随后启动 yarn build-hot
,您也看不到 JavaScript 的更改。
在 M1 机器上运行 Cypress
在 M1 机器上运行 Cypress 时可能会遇到问题。这是由使用 esbuild
作为依赖项的 @bahmutov/cypress-esbuild-preprocessor
引起的。错误可能看起来像这样。解决方案是使用像 nvm 或 n 这样的 Node 版本管理器安装 NodeJS。
您几乎肯定会遇到的另一个问题是无法连接到我们的 Mongo QA 数据库。您可以通过提供以下环境变量来解决此问题
export EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
运行依赖于 Docker 镜像的测试
我们的一部分测试依赖于通过 Docker 镜像提供的外部服务。在撰写本文时,这些是三个受支持的外部 QA 数据库、Webmail、Snowplow 和 LDAP 服务器。默认的 cypress 命令将启动所有必要的 docker 容器以使这些测试正常运行,但您可以根据需要将其关闭。
START_CONTAINERS=false yarn test-cypress
运行涉及 Snowplow 的测试
依赖 Snowplow 的测试需要一个运行中的服务器。这在默认情况下是启用的。您也可以通过启动 snowplow micro docker 容器并设置适当的环境变量来手动启用它们。
docker-compose -f ./snowplow/docker-compose.yml up -d
export MB_SNOWPLOW_AVAILABLE=true
export MB_SNOWPLOW_URL=https://:9090
使用 Snowplow 进行测试
我们有一些辅助函数来处理涉及 Snowplow 的测试
- 您可以使用
describeWithSnowplow
(或 EE 版本的describeWithSnowplowEE
)方法来定义仅在 Snowplow 实例运行时运行的测试 - 在每次测试之前使用
resetSnowplow()
测试辅助函数来清除已处理事件队列。 - 使用
expectSnowplowEvent({ ...payload }, count=n)
断言恰好有count
个 Snowplow 事件(部分)匹配提供的 payload(count 默认为 1) - 使用
expectUnstructuredSnowplowEvent
断言恰好有count
个 Snowplow 事件是非结构化事件,并且部分匹配提供的 payload。这只是一个方便的函数,用于比较event.unstruct_event.data.data
而不是整个event
。我们的大多数事件都是非结构化事件,所以这很方便。 - 使用
assertNoUnstructuredSnowplowEvent({ ...eventData })
是expectUnstructuredSnowplowEvent
的反向操作,它断言 *没有* 非结构化事件匹配 payload。 - 每次测试后使用
expectNoBadSnowplowEvents()
以断言没有发送无效事件。
运行需要 SMTP 服务器的测试
我们的一些测试依赖于电子邮件设置,并需要本地 SMTP 服务器。我们为此目的使用 maildev
Docker 镜像。在撰写本文时,我们使用的镜像是 maildev/maildev:2.1.0
。用于本地开发的默认 cypress 配置将为您处理此问题。如果您想手动设置,可以使用以下命令:
docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev:latest
Cypress 免费附带 Lodash
我们不需要在直接依赖中拥有 Lodash 即可在 Cypress 中使用它。它被别名为下划线,其方法可以通过 Cypress._.method()
访问。我们可以使用 _.times
方法在本地对某个测试(或一组测试)进行压力测试。
// Run the test N times
Cypress._.times(N, () => {
it("should foo", () => {
// ...
});
});
嵌入式 SDK 测试
数据库快照
在每个测试套件开始时,我们清除后端数据库和设置缓存。这确保了测试套件从一个可预测的状态开始。
通常,我们通过在第一个 describe
块中添加 before(restore)
来在运行整个测试套件之前进行恢复。如果您想使用除默认快照之外的快照,请将名称作为参数传递给 restore
,如下所示:before(() => restore("blank"))
。您也可以在 beforeEach()
中调用 restore()
以在每次测试之前或在特定测试中进行重置。
快照是使用一组独立的 Cypress 测试创建的。这些测试以空白数据库开始,并执行特定操作以使数据库处于可预测状态。例如:注册为 bob@metabase.com,添加问题,打开设置 ABC。
这些快照生成测试的扩展名为 .cy.snap.js
。当这些测试运行时,它们会在 frontend/tests/snapshots/*.sql
中创建数据库转储。它们在测试开始前运行,并且不会提交到 git。
在 CI 中运行
Cypress 记录每次测试运行的视频,这有助于调试。此外,失败的测试会保存更高质量的图像。
这些文件可以在 GitHub Actions 中每个运行摘要的“Artifacts”部分下找到。例如“Onboarding”目录中失败测试的 artifact:
针对 Metabase® Enterprise Edition™ 运行 Cypress 测试
在针对 Metabase® Enterprise Edition™ 运行 Cypress 之前,请设置 MB_EDITION=ee
环境变量。
企业实例将在没有高级令牌的情况下启动!
如果您想测试高级功能(功能标志),所有 Cypress 测试都需要提供有效的令牌。您应该提供 4 个令牌
- MB_ALL_FEATURES_TOKEN:启用所有功能,包括尚未向客户发布的新功能
- MB_STARTER_CLOUD_TOKEN:仅启用“托管”功能以模拟云上的初级套餐
- MB_PRO_CLOUD_TOKEN:启用 PRO 功能 + “托管”以模拟云上的专业套餐
- MB_PRO_SELF_HOSTED_TOKEN:启用 PRO 功能但没有“托管”以模拟专业自托管套餐
您可以通过环境变量或 cypress.env.json
文件配置这些(请参阅 cypress.env.json.example
获取示例)。
欲了解更多信息,请参阅 Metabase 定价页面。
如果您导航到 /admin/settings/license
页面,许可证输入字段应显示活动令牌。分享截图时请小心!
- 如果测试开始运行但缺少企业功能:请确保您使用的令牌已启用相应的功能标志。
- 如果令牌一切正常,请采取“核”措施,终止所有 Java 进程:运行
killall java
并重启 Cypress。
标签
Cypress 允许我们标记测试,以便轻松查找某些类别的标签。例如,我们可以使用 @external
标记所有需要外部数据库的测试,然后只运行这些测试:yarn test-cypress --env grepTags="@external"
。标签应以 @
开头,以便更容易将其与其他字符串区分开来。
目前使用的标签有
@external
- 需要外部 docker 容器才能运行的测试@actions
- 使用 Metabase 操作并修改数据源中数据的测试
如何对不稳定的修复进行压力测试?
在本地修复不稳定的测试并不意味着该修复在 GitHub 的 CI 环境中也能正常工作。唯一能确定修复有效的方法是在 CI 中进行压力测试。这就是 .github/workflows/e2e-stress-test-flake-fix.yml
的目的。它允许您在不等待完整构建完成的情况下,快速测试分支中的修复。
请按照以下步骤操作
准备
- 创建一个包含您建议的修复的新分支,并将其推送到远程
- 跳过创建 PR,或创建一个**草稿**拉取请求
手动触发压力测试工作流
- 前往
https://github.com/metabase/metabase/actions/workflows/e2e-stress-test-flake-fix.yml
- 点击“This workflow has a workflow_dispatch event trigger.”旁边的 Run workflow 触发器
- 在第一个字段“Use workflow from”中选择您自己的分支(此部分至关重要!)
- 复制并粘贴您要测试的 spec 的相对路径(例如
e2e/test/scenarios/onboarding/urls.cy.spec.js
) - 您无需用引号将其括起来 - 设置要运行测试的所需次数
- 根据文档,可选地提供 grep 过滤器
- 点击绿色的“Run workflow”按钮并等待结果
使用此工作流时需要牢记的事项
- 它将自动尝试从过去的提交/CI 运行中查找并下载以前构建的 Metabase uberjar 作为 artifact 存储。
- 它旨在用于不需要新的 Metabase uberjar 的纯 E2E 修复。
- 如果修复需要更改源代码(无论是后端还是前端),请改为打开一个常规 PR,并让 CI 首先运行所有测试。之后,您可以手动触发压力测试工作流,如上所述,它将自动从本次 CI 运行中下载新构建的 artifact。请记住,CI 需要首先完全完成运行。工作流使用 GitHub REST API,否则它看不到 artifact。
报告
每个 spec 都会自动生成独立的 Mocha 报告。它们存储在 cypress/reports/mochareports
中。请记住,根目录 cypress/
文件夹被 git 忽略!
当测试在 CI 中运行时,我们通过合并这些独立报告(使用 mochawesome-merge
),格式化它们,然后生成自定义的 GitHub Actions 作业摘要来执行一些额外的步骤。
如果您需要在 本地运行测试时 获得统一的测试报告,您可以通过调用 yarn generate-cypress-html-report
来实现。
阅读其他版本的 Metabase 的文档。