使用 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 实例,这甚至可能有助于调试。

标准开发流程

  1. 持续构建前端

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

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

  2. 在另一个终端会话中(不终止上一个会话)运行 yarn test-cypress。这将打开一个 Cypress GUI,让您选择要运行的测试。另外,您可以查看 run_cypress_local.jse2e/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 测试配置来设置视口宽度/高度。此配置适用于 describeit 块。

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

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

代码重载与测试重载

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

在“contains helper”打开时进行检查

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 时可能会遇到问题。这是由使用 esbuild 作为依赖项的 @bahmutov/cypress-esbuild-preprocessor 引起的。错误可能看起来像这样解决方案是使用 Node 版本管理器(如 nvmn)安装 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 的测试。

  1. 您可以使用 describeWithSnowplow(或 EE 版本的 describeWithSnowplowEE)方法来定义只有在 Snowplow 实例运行时才运行的测试。
  2. 在每个测试之前使用 resetSnowplow() 测试辅助函数清除已处理事件的队列。
  3. 使用 expectSnowplowEvent({ ...payload }, count=n) 断言正好有 count 个 Snowplow 事件(部分)匹配提供的 payload(count 默认为 1)。
  4. 使用 expectUnstructuredSnowplowEvent 断言正好有 count 个 Snowplow 事件是非结构化事件,并且部分匹配提供的 payload。这只是一个方便的函数,用于比较 event.unstruct_event.data.data 而不是整个 event。我们的大多数事件都是非结构化事件,因此这很方便。
  5. 使用 assertNoUnstructuredSnowplowEvent({ ...eventData })expectUnstructuredSnowplowEvent 的反义词,并断言*没有*非结构化事件匹配 payload。
  6. 在每个测试后使用 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”目录中失败测试的工件:GitHub Actions artifacts section

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

使用此工作流时需要牢记以下事项:

  • 它将自动尝试查找并下载之前构建的 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 版本的文档。