使用 Cypress 的端到端测试

Metabase 使用 Cypress 进行“端到端测试”,即针对整个应用程序进行的测试,包括前端、后端和应用程序数据库。这些测试本质上是由 JavaScript 编写的脚本,在浏览器中运行:访问不同的 URL、点击各种 UI 元素、输入文本,并断言事情按预期发生(例如,元素出现在屏幕上,或发生网络请求)。

入门指南

Metabase 的 Cypress 测试位于 e2e/test/scenarios 源树中,其结构大致反映了 Metabase 的 URL 结构。例如,管理“数据模型”页面的测试位于 e2e/test/scenarios/admin/datamodel

我们的自定义 Cypress 运行器会构建自己的后端并创建一个临时的 H2 应用程序数据库。这两个组件在进程终止时被销毁。保留的默认端口是本地主机上的 4000。没有任何阻止你在同一时间运行本地 Metabase 实例在 localhost:3000。这甚至可能有助于调试目的。

标准开发流程

  1. 持续构建前端

    a. 如果你只需要前端,运行 yarn build-hot

    b. 如果你想要在 Cypress 旁边运行本地 Metabase 实例,最简单的方法是使用 yarn devyarn dev-ee(两者都依赖于前端热重载)

  2. 在单独的终端会话中(不终止之前的会话)运行 yarn test-cypress-open。这将打开一个 Cypress GUI,允许你选择要运行的测试。另外,请参阅下面的更多运行选项。

运行选项

在终端中以编程方式运行所有 Cypress 测试

yarn run test-cypress-run

您可以使用自定义的 --folder 标志运行一组特定的场景,该标志将选择 e2e/test/scenarios/ 下的选择场景。

yarn run test-cypress-run --folder sharing

您可以通过使用官方的 --spec 标志快速测试单个文件。

yarn test-cypress-run --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/cypresscy.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

技巧/注意事项

containsfindget

Cypress 拥有一套用于选择元素的类似命令。以下是使用它们的技巧

  • contains 默认对 DOM 中的文本 敏感大小写。如果它不匹配您预期的文本,请检查 CSS 是否已更改大小写。您可以使用以下选项显式地告诉它忽略大小写: { matchCase: false }
    • contains 匹配子字符串。给定两个字符串“按筛选”和“添加筛选”,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 测试配置来设置视口宽度和高度。此配置与 describeit 块一起使用。

describe("foo", { viewportWidth: 1400 }, () => {});

it("bar", { viewportWidth: 1600, viewportHeight: 1200 }, () => {});

代码重新加载与测试重新加载

当您编辑 Cypress 测试文件时,测试将刷新并再次运行。然而,当您编辑代码文件时,Cypress 不会检测到该更改。如果您正在运行 yarn build-hot,则代码将在 Cypress 中重建并更新。新代码加载后,您必须手动点击重新运行。

在“contains 辅助函数”打开时进行检查

Cypress 的一大亮点是您可以在测试的每一步之后使用 Chrome 检查器。它们还提供了有用的助手,可以测试 containsget 调用。此助手创建新的 UI,防止检查器定位到正确的元素。如果您想检查 Chrome 中的 DOM,应关闭此助手。

将错误的 HTML 模板放入 Uberjar

命令 yarn buildyarn build-hot 都会覆盖 HTML 模板以引用正确的 JavaScript 文件。如果您在为 Cypress 测试构建 Uberjar 之前运行 yarn build,即使您随后启动 yarn build-hot,也不会看到 JavaScript 的更改。

在 M1 机器上运行 Cypress

在 M1 机器上运行 Cypress 时可能会遇到问题。这是由于作为依赖项使用的 @bahmutov/cypress-esbuild-preprocessor 导致的,该依赖项使用 esbuild。错误可能看起来像这样这样。解决方案是使用类似于 nvmn 的 Node 版本管理器安装 NodeJS。

您几乎肯定会遇到另一个问题是无法连接到我们的 Mongo QA 数据库。您可以通过提供以下环境变量来解决这个问题

export EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1

运行依赖于 Docker 镜像的测试

我们的部分测试依赖于通过 Docker 镜像提供的的外部服务。在撰写本文时,这些是三个受支持的 QA 数据库:Webmail、Snowplow 和 LDAP 服务器。本地运行所有这些 Docker 容器很麻烦。为那些不在乎这些测试但需要在本地运行包含它们的规范的人提供了逃生机制。运行以下命令

yarn test-cypress-run --env grepTags="-@external" --spec path/to/spec/foo.cy.spec.js

请注意 @external 标签之前的减号。有关更多详细信息,请参阅官方文档

如果您想运行或需要运行这些测试,有一个方便的选项可以为您完成繁重的工作

yarn test-cypress-open-qa

涉及 Snowplow 的测试运行

依赖于 Snowplow 的测试期望有一个运行中的服务器。要运行它们,您需要

  • 将环境变量传递给测试运行:MB_SNOWPLOW_AVAILABLE=true MB_SNOWPLOW_URL=https://127.0.0.1:9090 yarn test-cypress-open

使用 Snowplow 进行测试

我们的端到端测试环境已经配置为与应用程序一起运行 Snowplow Micro。

要本地运行 Snowplow,请使用以下命令

docker-compose -f ./snowplow/docker-compose.yml up -d
export MB_SNOWPLOW_AVAILABLE=true
export MB_SNOWPLOW_URL=https://127.0.0.1:9090
  1. 您可以使用 describeWithSnowplow(或 EE 版本的 describeWithSnowplowEE)方法来定义仅在 Snowplow 实例运行时才运行的测试
  2. 在每个测试之前使用 resetSnowplow() 测试助手来清除处理事件队列。
  3. 使用 expectGoodSnowplowEvents(count) 来断言事件已正确发送和处理。使用 expectGoodSnowPlowEvent({ ...payload}) 来断言 snowplow 事件的内容
  4. 在每次测试后使用expectNoBadSnowplowEvents()来断言没有发送无效事件。

运行需要SMTP服务器的测试

我们的一些测试依赖于电子邮件设置,需要本地SMTP服务器。为此,我们使用maildev Docker镜像。截至本文撰写时,我们使用的镜像为maildev/maildev:2.1.0。在本地开发中始终使用:latest镜像应该是安全的。运行以下命令:

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设置文档

数据库快照

在每个测试套件开始时,我们清除后端的数据库和设置缓存。这确保测试套件从可预测的状态开始。

通常,我们在第一个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”目录中的示例:![GitHub Actions artifacts section](https://user-images.githubusercontent.com/31325167/241774190-f19da1d5-8fca-4c48-9342-ead18066bd12.png)

针对Metabase® Enterprise Edition™运行Cypress测试

在运行Cypress针对Metabase® Enterprise Edition™之前,设置环境变量MB_EDITION=ee。我们有一个特殊的describe块,称为describeEE,它将根据版本有条件地跳过或运行测试。

企业实例将启动而没有高级令牌!

如果您想测试高级功能(功能标志),所有 Cypress 测试都需要有效的令牌。我们通过在环境变量前加上 CYPRESS_ 前缀来实现这一点。您必须提供两个与 EE/PRO 自托管(所有功能启用)和 STARTER 云(无功能启用)Metabase 计划对应的令牌。有关更多信息,请参阅 Metabase 定价页面

  • CYPRESS_ALL_FEATURES_TOKEN
  • CYPRESS_NO_FEATURES_TOKEN
MB_EDITION=ee CYPRESS_ALL_FEATURES_TOKEN=xxxxxx CYPRESS_NO_FEATURES_TOKEN=xxxxxx yarn test-cypress-open

如果您导航到 /admin/settings/license 页面,许可证输入字段应显示活动令牌。分享截图时请小心!

  • 如果 describeEE 块下的测试变灰且未运行,请确保您启动了 Metabase® 企业版™。
  • 如果测试开始运行但缺少企业功能:请确保您使用的令牌已启用相应的功能标志。
  • 如果令牌似乎一切正常,那么就采取极端措施,销毁所有 Java 进程:运行 killall java 并重新启动 Cypress。

标签

Cypress 允许我们 标记 测试,以便轻松找到特定类别的标签。例如,我们可以用 @external 标记所有需要外部数据库的测试,然后只运行这些测试:运行 yarn test-cypress-open --env grepTags="@external"。标签应从 @ 开始,以便更容易在搜索中将其与其他字符串区分开来。

这些是目前正在使用的标签

  • @external - 需要外部 docker 容器运行的测试
  • @actions - 使用 metabase 动作并在数据源中修改数据的测试

如何对 flake 修复进行压力测试?

在本地修复 flaky 测试并不意味着修复在 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 事件触发器”
  1. 在第一个字段“使用工作流程从”中选择您自己的分支(这部分非常重要!)
  2. 复制并粘贴您想要测试的 spec 的相对路径(例如,e2e/test/scenarios/onboarding/urls.cy.spec.js)- 您无需将其括在引号内
  3. 设置测试运行的期望次数
  4. 可选地提供 grep 过滤器,根据 文档 进行设置
  5. 点击绿色的“运行工作流程”按钮,等待结果

使用此工作流程时需要注意的事项

  • 它将自动尝试找到并下载以前构建的 Metabase uberjar,该 uberjar 存储为过去提交/CI 运行的工件。
  • 本意是用于仅需要端到端修复且不需要新Metabase uberjar的场景。
  • 如果修复需要源代码更改(无论是后端还是前端),请打开一个常规的PR,并先让CI运行所有测试。之后,您可以手动触发如上所述的压力测试工作流,它将自动下载CI运行中新建的工件。请注意,CI需要完全运行完成后。工作流使用GitHub REST API,否则无法看到工件。

阅读其他Metabase版本的文档。

想要改进这些文档吗? 提出更改。