使用 Cypress 进行端到端测试
Metabase 使用 Cypress 进行“端到端测试”,即针对整个应用程序(包括前端、后端和应用程序数据库)执行的测试。这些测试本质上是用 JavaScript 编写并在 Web 浏览器中运行的脚本:访问不同的 URL、点击各种 UI 元素、输入文本,并断言事情按预期发生(例如,屏幕上出现某个元素,或发生网络请求)。
入门
Metabase 的 Cypress 测试位于 e2e/test/scenarios
源目录中,其结构大致模仿 Metabase 的 URL 结构。例如,管理员“数据模型”页面的测试位于 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 测试的浏览器。有关更多详细信息,请参阅官方文档。
在 *run* 模式下运行 Cypress 时,指定浏览器最有意义。另一方面,Cypress *open* 模式(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 }, () => {});
代码重载与测试重载
当您编辑 Cypress 测试文件时,测试将刷新并再次运行。但是,当您编辑代码文件时,Cypress 不会检测到该更改。如果您正在运行 yarn build-hot
,代码将在 Cypress 中重新构建和更新。在新代码加载后,您必须手动点击重新运行。
在“contains helper”打开时进行检查
Cypress 的一个强大功能是您可以在测试的每个步骤之后使用 Chrome 检查器。它们还提供了一个有用的辅助工具,可以测试 contains
和 get
调用。此辅助工具会创建新的 UI,从而阻止检查器定位到正确的元素。如果您想在 Chrome 中检查 DOM,则应关闭此辅助工具。
将错误的 HTML 模板放入 Uberjar
yarn build
和 yarn build-hot
都会覆盖 HTML 模板以引用正确的 JavaScript 文件。如果您在为 Cypress 测试构建 Uberjar 之前运行 yarn build
,即使您之后启动 yarn build-hot
,您也看不到 JavaScript 的更改。
在 M1 机器上运行 Cypress
您在 M1 机器上运行 Cypress 时可能会遇到问题。这是由使用 esbuild
作为依赖项的 @bahmutov/cypress-esbuild-preprocessor
引起的。错误可能看起来像这样。解决方案是使用 Node 版本管理器(如 nvm 或 n)安装 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=http://localhost: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 测试
位于 e2e/test-component/scenarios/embedding-sdk/
中的测试用于运行嵌入式 SDK 的自动化检查。
要在本地运行测试,请参阅SDK 文档中的 e2e 部分。
示例应用程序与嵌入式 SDK 测试的兼容性
为了检查示例应用程序和嵌入式 SDK 之间的兼容性,我们为每个示例应用程序创建了一个特殊的测试套件,该套件会拉取此示例应用程序,启动它并针对本地的 metabase.jar
和本地的 @metabase/embedding-sdk-react
包运行其 Cypress 测试。
本地运行
要本地运行这些测试,请运行
ENTERPRISE_TOKEN=<token> TEST_SUITE=<sample_app_repo_name>-e2e OPEN_UI=false EMBEDDING_SDK_VERSION=local START_METABASE=false GENERATE_SNAPSHOTS=false START_CONTAINERS=false yarn test-cypress
例如,对于 metabase-nodejs-react-sdk-embedding-sample
,请运行
ENTERPRISE_TOKEN=<token> TEST_SUITE=metabase-nodejs-react-sdk-embedding-sample-e2e OPEN_UI=false EMBEDDING_SDK_VERSION=local START_METABASE=false GENERATE_SNAPSHOTS=false START_CONTAINERS=false yarn test-cypress
:warning: 本地获取 Shoppy 的 Metabase 应用数据库转储
对于 Shoppy 示例应用程序测试 (TEST_SUITE=shoppy-e2e
) 在本地运行,必须将 Shoppy Metabase 实例的正确应用程序数据库转储放置在 ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
。
您可以通过以下方式获取它:
- 启用
Tailscale
并使用您的工作电子邮件地址登录。 - 运行
pg_dump "postgres://:@:/" > ./e2e/tmp/db_dumps/shoppy_metabase_app_db_dump.sql
命令。- 在
1password
中查看Shoppy Coredev Appdb
记录以获取凭据。
- 在
CI 运行
在我们的 CI 中,测试失败不会阻止拉取请求(PR)的合并。但是,如果测试失败,很可能是由于以下原因之一:
-
构建失败:
故障发生在本地
@metabase/embedding-sdk-react
dist 的构建过程中。这表明前端代码中可能存在语法或类型错误。 -
测试运行失败:
失败发生在实际的测试执行期间。在这种情况下,PR 可能引入了更改,导致以下情况之一:
- 破坏了整个 Metabase 或嵌入式 SDK,或者
- 破坏了嵌入式 SDK 和示例应用之间的兼容性。
如果一个 PR 破坏了嵌入式 SDK 和示例应用之间的兼容性,该 PR 仍然可以合并。但是,对于每个受影响的示例应用,应创建一个单独的 PR,以在新的 @metabase/embedding-sdk-react
版本发布时恢复兼容性。这些兼容性 PR 只有在包含破坏性更改的嵌入式 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”目录中失败测试的工件:
针对 Metabase® 企业版™ 运行 Cypress 测试
在针对 Metabase® 企业版™ 运行 Cypress 之前,请设置 MB_EDITION=ee
环境变量。
企业版实例将在没有高级令牌的情况下启动!
如果您想测试高级功能(功能标志),有效的令牌需要对所有 Cypress 测试可用。我们通过在环境变量前加上 CYPRESS_
来实现这一点。您应该提供两个令牌,分别对应于 EE/PRO
自托管(所有功能启用)和 STARTER
云(无功能启用)Metabase 计划。有关更多信息,请参阅Metabase 定价页面。(注意:只有少数测试需要无功能令牌)
CYPRESS_ALL_FEATURES_TOKEN
CYPRESS_NO_FEATURES_TOKEN
MB_EDITION=ee ENTERPRISE_TOKEN=xxxxxx yarn test-cypress
如果您导航到 /admin/settings/license
页面,许可证输入字段应显示活动令牌。分享截图时请小心!
- 如果测试开始运行但缺少企业功能:请确保您使用的令牌已启用相应的功能标志。
- 如果令牌一切正常,请彻底关闭所有 Java 进程:运行
killall java
并重新启动 Cypress。
标签
Cypress 允许我们标记测试,以便轻松查找某些类别的标签。例如,我们可以用 @external
标记所有需要外部 Docker 容器才能运行的测试,然后只运行这些测试,命令为 yarn test-cypress --env grepTags="@external"
。标签应以 @
开头,以方便在搜索中区分它们与其他字符串。
以下是当前使用的标签
@external
- 需要外部 Docker 容器才能运行的测试@actions
- 使用 Metabase 操作并修改数据源中数据的测试
如何对偶然失败的修复进行压力测试?
在本地修复一个偶然失败的测试并不意味着该修复在 GitHub 的 CI 环境中也能奏效。确定修复有效的唯一方法是在 CI 中对其进行压力测试。这就是 .github/workflows/e2e-stress-test-flake-fix.yml
的作用。它允许您在自己的分支中快速测试修复,而无需等待完整构建完成。
请遵循以下步骤
准备
- 创建一个包含您建议的修复的新分支并将其推送到远程
- 跳过打开 PR 或打开**草稿** PR
手动触发压力测试工作流
- 前往
https://github.com/metabase/metabase/actions/workflows/e2e-stress-test-flake-fix.yml
- 点击“此工作流具有 workflow_dispatch 事件触发器。”旁边的*运行工作流*触发器
- 在第一个字段“使用来自的工作流”中选择您自己的分支(这部分至关重要!)
- 复制并粘贴您要测试的 spec 的相对路径(例如
e2e/test/scenarios/onboarding/urls.cy.spec.js
) - 您无需将其用引号括起来 - 设置测试的所需运行次数
- 根据文档,可选地提供一个 grep 过滤器
- 点击绿色的“运行工作流”按钮并等待结果
使用此工作流时需要牢记以下事项:
- 它将自动尝试查找并下载之前构建的 Metabase uberjar,该 uberjar 作为工件存储在过去的提交/CI 运行中。
- 它旨在用于不需要新的 Metabase uberjar 的纯 E2E 修复。
- 如果修复需要源代码更改(无论是后端还是前端),请改为打开常规 PR,并让 CI 首先运行所有测试。之后,您可以手动触发压力测试工作流,如上所述,它将自动从本次 CI 运行中下载新构建的工件。请记住,CI 需要首先完全完成运行。否则,该工作流使用的 GitHub REST API 将无法看到工件。
报告
每个 spec 会自动生成单独的 Mocha 报告。它们存储在 cypress/reports/mochareports
中。请注意,根级别的 cypress/
文件夹被 Git 忽略!
当测试在 *CI 中*运行时,我们会采取一些额外步骤,通过合并这些单独的报告(使用 mochawesome-merge
)、格式化它们,然后生成定制的 GitHub Actions 作业摘要。
万一您在*本地运行测试时*需要统一的测试报告,可以通过调用 yarn generate-cypress-html-report
来实现。
阅读其他 Metabase 版本的文档。