使用 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 实例。这甚至可能对调试目的有所帮助。

标准开发流程

  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 标志快速仅测试单个文件。此标志可用于运行文件夹中的所有规范,或运行多个不同的规范。请查阅 官方文档 获取说明。

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 测试配置设置视口宽度/高度。此配置适用于 describeit 块。

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

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

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

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

在“包含助手”打开时检查

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 作为依赖项引起的。错误可能看起来 像这样解决方案 是使用 Node 版本管理器(如 nvmn)之一安装 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 的测试的助手

  1. 您可以使用 describeWithSnowplow(或 EE 版本的 describeWithSnowplowEE)方法来定义仅在 Snowplow 实例运行时运行的测试
  2. 在每次测试之前使用 resetSnowplow() 测试助手来清除已处理事件的队列。
  3. 使用 expectGoodSnowPlowEvent({ ...payload}) 来断言 snowplow 事件的内容。使用 expectGoodSnowplowEvents(count) 来断言事件已发送并已正确处理。首选对实际有效负载进行更精确的断言,而不是仅仅对事件进行计数。
  4. 在每次测试后使用 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”目录中失败测试的工件示例:GitHub Actions artifacts section

针对 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 事件触发器”旁边的运行工作流程触发器。
  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 版本的文档。