简体中文
  • 测试
  • 端到端测试
  • 集成测试
  • 开发
  • 生产力
  • jest
  • puppeteer

使用 jest-puppeteer 编写端到端测试的快速指南

本文提供了一个使用 jest-puppeteer 编写高效端到端测试的快速指南,重点介绍了设置过程、常用 API 以及使用一个简易待办事项应用作为示例进行实际测试的场景。

Yijun
Yijun
Developer

为了确保 Logto 的质量并持续改进产品,我们使用 jest-puppeteer 进行端到端自动化测试。这样可以让我们快速迭代 Logto 的开发过程而不受干扰。

在本文中,我们将分享使用一个简易待办事项应用作为示例编写高效 jest-puppeteer 测试脚本的经验。我们旨在帮助你快速入门,为自己的项目编写 jest-puppeteer 测试代码。

使用 jest-puppeteer 进行端到端测试简介

端到端测试是一种确保应用从用户角度正确运行的方法。为此,我们使用了两个基本工具:JestPuppeteer

Jest 是一个广受欢迎的 JavaScript 测试框架,提供了一个用户友好的 API 用于编写测试和断言。它被广泛用于单元测试和集成测试。

Puppeteer 是由 Chrome 团队开发的 Node.js 库,提供了一个高层次的 API 用于控制无头 Chrome 或 Chromium 浏览器。使其成为在端到端测试中自动化浏览器操作的理想选择。

jest-puppeteer 是一个 Jest 预设,支持使用 Puppeteer 进行端到端测试。它提供了一个简单的 API 用于启动新的浏览器实例并通过它们与网页进行交互。

现在你对这些基本工具有了初步了解,让我们深入了解如何设置你的测试环境。

设置你的测试环境

使用 jest-puppeteer 进行端到端测试的环境设置是一个简单的过程,包括三个主要步骤:

  1. 安装依赖

在你的项目中(或者创建一个新项目),打开终端并运行以下命令:

然后进入 node_modules/puppeteer 目录并安装 Chromium(Puppeteer 需要):

  1. 配置 Jest

接下来,你需要配置 Jest 以与 Puppeteer 无缝协作。

如果你的项目根目录中还没有 Jest 配置文件,请创建一个(例如,jest.config.js)。在该配置文件中,将 jest-puppeteer 指定为预设:

你可以根据需要在此文件中自定义其他 Jest 设置。有关自定义 Jest 配置的更多信息,请参阅 Jest 配置

  1. 编写你的测试

在你的项目中创建测试文件,通常使用 .test.js 作为文件扩展名。Jest 会自动发现并执行这些测试文件。

以下是来自 Jest 文档 的示例:

然后执行以下命令运行测试:

通过这三个步骤,你将拥有一个配置良好的测试环境,以使用 jest-puppeteer 进行端到端测试。

不过,请注意,这只是一个基本示例。有关环境配置的更多详细信息,请参阅相关文档:

常用 API

在接下来的步骤中,我们将依赖 Jest、Puppeteer 和 jest-puppeteer 提供的 API 来帮助我们进行测试。

Jest 主要提供用于组织测试和断言预期结果的 API。你可以在 文档 中探索具体细节。

Puppeteer 的 API 主要用于与浏览器交互,并且它们对测试的支持可能不太直观。你可以参考 Puppeteer 文档 了解其提供的功能。在我们后续的测试示例中,我们将涵盖一些常见的用例。

由于 Puppeteer 的 API 最初并非针对测试设计,使用这些 API 编写测试可能具有一定挑战性。为简化编写 Puppeteer 测试的过程,jest-puppeteer 包含了一个内嵌库 expect-puppeteer,它提供了一套简洁且用户友好的 API。该库的更多详情见其 文档,下面的示例展示了其一些有用功能:

在接下来的示例中,我们将结合这些库提供的 API 来完成我们的测试场景。

现在是时候开始学习如何使用我们简易待办事项应用编写测试代码了。

简易待办事项应用

假设我们有一个运行在 http://localhost:3000 的简易待办事项应用,其主要的 HTML 代码如下:

当进行端到端测试时,我们希望覆盖以下场景:

  1. 访问应用的 URL 时,应用及其数据应该正确加载。
  2. 项目的勾选按钮应该可以点击。
  3. 点击项目备注中的外部链接应该会在新标签页中打开链接。
  4. 可以从表单中添加一个项目。

接下来,我们将编写测试代码以验证这些场景。

期待应用及其数据已加载

我们认为在满足以下条件时应用已成功加载:

  1. 访问应用的 URL 后,页面上应该存在一个 ID 为 "app" 的元素。
  2. 应用内的数据应正确渲染。

因此,我们编写了如下测试代码:

在代码中:

  • page.goto 相当于在浏览器中输入 "http://localhost:3000",可以让你导航到任何 URL。
  • page.waitForSelector 用于等待特定 CSS 选择器与某一元素匹配,默认等待时间为 30 秒。
  • expect(page).toMatchElement 来自 expect-puppeteer 库,它类似于 page.waitForSelector,但它还支持匹配元素内的文本。此外,toMatchElement 的默认超时时间只有 500 毫秒。

乍一看似乎很完美,但在运行此测试并将其部署到 CI 环境时,多次执行后偶尔会失败。失败信息显示:

根据错误信息以及该测试大多数时间都通过的事实,我们可以推断出应用请求的数据可能并不总是在应用加载后的前 500 毫秒内返回。因此,我们希望等待数据加载完成后再进行断言。

通常,有两种常见的方法来实现这一目标:

  1. 增加断言等待时间

从错误消息中,我们可以看到 toMatchElement 的默认等待时间设置为 500 毫秒。我们可以通过将 timeout 选项添加到函数中来增加等待时间,如下所示:

这种方法可以在一定程度上减少测试失败的出现,但它不能完全解决问题,因为我们无法确切知道数据获取需要的时间。

因此,我们只在确定所需等待时间的情况下使用此方法,例如在某些像“鼠标悬停超过 2 秒后出现提示框”的场景中。

  1. 在进行断言前等待网络请求完成

这是正确的方法。虽然我们可能不知道数据请求需要多长时间,但等待网络请求完成总是一个安全的选择。在这一点上,我们可以使用 page.waitForNavigation({ waitUntil: 'networkidle0' }) 等待网络请求完成:

这样,在我们对应用及其加载数据进行断言之前,我们可以确保我们的网络请求已经完成。这确保了测试始终产生正确的结果。

期待点击一个特定的按钮

接下来,我们将测试点击某个项目中的勾选按钮的功能。

在示例应用中,我们注意到所有项目都有相同的结构。它们的勾选按钮都有 CSS 选择器 li[class$=item] > button,并且按钮文本总是"勾选"。这意味着我们不能直接指定点击哪个项目的勾选按钮。因此,我们需要找到一个新的解决方案。

假设我们想要点击"阅读 Logto 入门文档"项目的勾选按钮。我们可以将其分解为两个步骤:

  1. 首先,获取这个特定项目的引用。
  2. 然后,点击该项目下的勾选按钮。

如代码所示,我们使用 toMatchElement 函数匹配 itemName 设置为"阅读 Logto 入门文档"的项目。然后,我们使用 readDocItem 引用点击其下的文本内容为'勾选'的按钮。

需要注意的是,在获取 readDocItem 时,使用的 CSS 选择器是 li[class$=item]:has(div[class$=itemName])。该选择器匹配的是项目的根 li 元素,而非 itemName 之内,因为我们打算稍后点击 li 标记下的 button

expect(readDocItem).toClick 的用法类似于 toMatchElement。在示例代码中,我们传递 { text: '勾选' } 以进一步匹配按钮的 textContent。但是,你可以根据需要决定是否匹配按钮的文本内容。

期待外部链接在新标签中打开

接下来,我们要测试在项目备注中点击外部链接时,是否可以在新标签页中打开链接。

在示例应用中,我们发现 "阅读 Logto 入门文档" 项目在其备注中有一个指向 Logto 文档的外部链接。以下是我们的测试代码:

在代码中,我们使用 toClick 点击了 itemNotes 中的链接。

随后,我们使用 browser.waitForTarget 捕获一个 URL 为 "https://docs.logto.io/docs/tutorials/get-started" 的新开的标签页。

在获取该标签页后,我们使用 target.page() 获取 Logto 文档页面的实例,并进一步检测页面是否已加载。

还要记得在完成测试后关闭新打开的页面。

期待从表单中创建一个项目

现在,我们要测试添加项目的功能。

我们需要在表单中填写项目名称和项目备注,点击"添加"按钮,并检查我们添加的内容是否出现在列表中:

你会注意到我们使用 expect(page).toFill(inputSelector, content) 函数来填写表单内容。此函数会用 content 替换输入框中的所有内容。

如果你希望在不替换现有内容的情况下向输入框中添加一些字符,可以使用 page.type(selector, content) 将所需的内容直接附加到输入框中。

但是,当我们需要填写多个字段时,多次调用 toFill 不太方便。在这种情况下,我们可以使用以下方法一次性填写所有内容:

提供的对象键就是 input 元素的 name 属性。

总结

我们介绍了一个简单的示例,以解释使用 jest-puppeteer 进行端到端测试时的常见测试需求及其相应的代码编写技巧。我们希望它可以帮助你快速掌握如何编写 jest-puppeteer 测试代码的基础。