使用 Cypress 进行端到端测试
Metabase 使用 Cypress 进行“端到端测试”,即针对整个应用程序(包括前端、后端和应用程序数据库)执行的测试。这些测试本质上是用 JavaScript 编写的脚本,在 Web 浏览器中运行:访问不同的 URL,单击各种 UI 元素,键入文本,并断言事情按预期发生(例如,屏幕上出现元素或发生网络请求)。
入门指南
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
标志快速仅测试单个文件。此标志可用于运行文件夹中的所有规范,或运行多个不同的规范。请查阅 官方文档 获取说明。
OPEN_UI=false yarn test-cypress --spec e2e/test/scenarios/question/new.cy.spec.js
您可以使用 --browser
标志指定要在其中执行 Cypress 测试的浏览器。有关更多详细信息,请查阅 官方文档。
指定浏览器在以运行模式运行 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 中重建和更新。您必须在新代码加载后手动单击重新运行。
在“包含助手”打开时检查
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 时,您可能会遇到问题。这是由 @bahmutov/cypress-esbuild-preprocessor
使用 esbuild
作为依赖项引起的。错误可能看起来 像这样。 解决方案 是使用 Node 版本管理器(如 nvm 或 n)之一安装 NodeJS。
您几乎肯定会面临的另一个问题是无法连接到我们的 Mongo QA 数据库。您可以通过提供以下 env 来解决它
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://127.0.0.1:9090
使用 Snowplow 进行测试
我们有一些用于处理涉及 snowplow 的测试的助手
- 您可以使用
describeWithSnowplow
(或 EE 版本的describeWithSnowplowEE
)方法来定义仅在 Snowplow 实例运行时运行的测试 - 在每次测试之前使用
resetSnowplow()
测试助手来清除已处理事件的队列。 - 使用
expectGoodSnowPlowEvent({ ...payload})
来断言 snowplow 事件的内容。使用expectGoodSnowplowEvents(count)
来断言事件已发送并已正确处理。首选对实际有效负载进行更精确的断言,而不是仅仅对事件进行计数。 - 在每次测试后使用
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/scenarios/embedding-sdk
中的测试用于运行嵌入 SDK 的自动化检查。
嵌入 SDK 是一个库,而不是应用程序。我们使用 Storybook 来托管公共组件,并针对它运行测试。
为了在本地运行用于测试的故事,请查看 storybook 设置文档
DB 快照
在每个测试套件开始时,我们都会擦除后端的数据库和设置缓存。这确保了测试套件以可预测的状态启动。
通常,我们通过在第一个 describe
块内添加 before(restore)
来使用默认快照,以便在运行整个测试套件之前恢复。如果您想使用默认快照以外的快照,请像这样将名称指定为 restore
的参数:before(() => restore("blank"))
。您也可以在 beforeEach()
中调用 restore()
以在每次测试之前重置,或在特定测试中调用。
快照是通过一组单独的 Cypress 测试创建的。这些测试从空白数据库开始,并执行特定操作以使数据库处于可预测状态。例如:注册为 [email protected],添加问题,打开设置 ABC。
这些快照生成测试具有扩展名 .cy.snap.js
。当这些测试运行时,它们会在 frontend/tests/snapshots/*.sql
中创建数据库转储。它们在测试开始之前运行,并且不会提交到 git。
在 CI 中运行
Cypress 记录每次测试运行的视频,这有助于调试。此外,失败的测试具有更高质量的保存图像。
这些文件可以在 GitHub Actions 中每个运行摘要的“Artifacts”部分下找到。“Onboarding”目录中失败测试的工件示例:
针对 Metabase® Enterprise Edition™ 运行 Cypress 测试
在针对 Metabase® Enterprise Edition™ 运行 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
标记所有需要外部数据库的测试,然后仅使用 yarn test-cypress --env grepTags="@external"
运行这些测试。标签应以 @
开头,以便更容易将它们与搜索中的其他字符串区分开来。
以下是当前正在使用的标签
@external
- 需要外部 Docker 容器才能运行的测试@actions
- 使用 Metabase actions 并在数据源中更改数据的测试
如何压力测试 flake 修复?
在本地修复一个不稳定的测试并不意味着该修复在 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
- 单击“此工作流程具有 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 版本的文档。