使用 Cypress 进行端到端测试
Metabase 使用 Cypress 进行“端到端测试”,即针对整个应用程序执行的测试,包括前端、后端和应用程序数据库。这些测试本质上是用 JavaScript 编写的脚本,在 Web 浏览器中运行:访问不同的 URL,单击各种 UI 元素,输入文本,并断言事件是否如预期发生(例如,屏幕上出现某个元素,或发生网络请求)。
请在继续之前熟悉 Cypress 最佳实践。
入门
Metabase 的 Cypress 测试位于 e2e/test/scenarios 源代码树中,其结构大致反映了 Metabase 的 URL 结构。例如,针对管理员“数据模型”页面的测试位于 e2e/test/scenarios/admin/datamodel。
我们的自定义 Cypress 运行器会构建自己的后端并创建一个临时的 H2 应用数据库。当该进程终止时,两者都会被销毁。本地主机上的保留默认端口为 4000。同时运行您本地的 Metabase 实例在 localhost:3000 上也没有问题。这甚至可能有助于调试。
标准开发流程
-
持续构建前端
a. 如果您只需要前端,请运行
yarn build-hotb. 如果您想在运行 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 的打开模式(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() 等助手直接跳转到那里,而不是从主页开始,单击“New”,然后“Question”等。
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 与 find 与 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 引起的。错误可能看起来如此。 解决方案是使用像 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 的测试的助手。
- 在每个测试之前使用
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 测试
请参阅 SDK 关于 e2e 的文档。
数据库快照
在每个测试套件开始时,我们会清除后端的数据库和设置缓存。这确保了测试套件以可预测的状态启动。
通常,我们使用默认快照,通过在第一个 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® 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 功能 + ‘hosting’ 以模拟云端 PRO 计划
- MB_PRO_SELF_HOSTED_TOKEN: 启用 PRO 功能但禁用 ‘hosting’ 以模拟本地部署的 PRO 计划
您可以通过环境变量 (ENV) 或 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”按钮,等待结果
使用此工作流时需要注意的事项
- 它将自动尝试查找并下载先前构建的 Metabase uberjar,该 jar 存储为过去提交/CI 运行的工件。
- 它旨在用于纯粹的 E2E 修复,而无需新的 Metabase uberjar。
- 如果修复需要源代码更改(后端或前端),请改用常规 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 的文档。