Web 应用端到端测试:无痛指南
原文由 Michael Lynch 于 发布,订阅该博客
我知道你持怀疑态度。其他指南也曾承诺让你无痛地测试 Web 应用,结果却发现它们的方案要求某种极为特定的技术栈,或是付费的第三方服务。我不会这么做。
本指南提供了一套简单灵活的端到端测试模板,几乎可以应用于任何 Web 应用。唯一的要求是你的应用能在 Docker 中运行。
真的就只有这一个要求!你可以测试 Ruby 应用、React 应用、Enterprise Java Beans 应用,甚至是你自己捣鼓出来的奇葩技术栈。而且无论你在 Windows、Linux 还是 Mac 上开发都没关系。最棒的是,你无需进行繁琐的配置,除了 Docker 之外也不用再安装任何软件。
本教程使用的是免费的开源工具,无需在任何地方注册账号就能运行。当需要在 Circle 或 Travis 这类持续集成环境中运行测试时,你也不需要做任何特殊处理——只需用和你在开发机上一样的一行命令就能跑起测试。
Cypress,本场主角
更新(2022-10-25):我已不再推荐使用 Cypress 进行 Web 应用的端到端测试。对于新项目,我推荐改用 Playwright。
让这一切成为可能的是 Cypress,一款浏览器自动化领域的新秀。它是一个开源的端到端测试框架,由一支全职团队持续维护。其商业模式与 Docker 类似,两家公司都发布免费的开源工具,并通过销售相关托管服务来支撑开发。

Cypress 是一款用于 Web 应用自动化测试的开源工具。
我去年在一场地区性软件大会上看到 Gleb Bahmutov 演示 Cypress,才第一次了解到它。当他提到 Cypress 完全不依赖 Selenium 时,我立刻产生了兴趣。我过去所有端到端测试的体验都糟糕透顶,而痛苦的根源总是 Selenium。

Selenium 是历史最久、也最流行的浏览器自动化工具,但它笨重且陈旧。
Selenium 是迄今为止最流行的浏览器自动化框架,但它也具备你能想象到的、一个 15 年前设计的基于 Java 的工具的所有问题:安装麻烦、语法别扭、测试失败时几乎不提供任何有用的诊断信息。在 Gleb 演示的那些流畅的 Cypress 示例中,它承诺要解决所有这些痛点。

Cypress 的一个亮点功能是在测试的每一步都会录制浏览器画面,方便你排查失败原因。
我迫不及待地阅读了 Cypress 文档,却失望地发现,几乎所有文档都默认用户使用的是 Node.js 技术栈,并且是在图形化环境而非无头控制台中开发。
不过,Cypress 依然前景可期。一年后,我再次关注它的进展,发现了一个结合了 Cypress 与 Docker Compose 的新示例应用。顿时一切都豁然开朗。一旦看到 Cypress 在 Docker Compose 下的运行方式,就能清楚该如何把这一模式套用到任何 Web 应用上。今天,我就来向你展示这个模式以及如何在你的应用中使用它。
可复用的端到端测试模式
将 Cypress 与 Docker Compose 结合,能得到一套足够灵活、几乎适用于任何 Web 应用的测试模式。与那些对应用实现有所假设的其他测试工具不同,这套方案将测试框架与被测应用完全解耦。

Docker Compose、Cypress 与 Web 应用如何协同工作
借助 Docker Compose,你可以在一个容器中运行 Cypress,在另一个容器中运行你的应用。你的应用完全不需要了解 Cypress,而 Cypress 唯一需要知道的,就是向你的应用发送 HTTP 请求的网络端口。
一个用于测试的简单 Web 应用
作为待测的示例 Web 应用,我来介绍 Sentimentalyzer:世界上最笨的文本情感分析器。它会尝试根据用户输入的一段文字来猜测其心情。
如果你输入文本 It's a nice day today,Sentimentalyzer 会判定你很开心:


Sentimentalyzer 分析开心文本
如果你输入文本 Who ate ALL MY WAFFLES?,Sentimentalyzer 则会认为你在生气:


Sentimentalyzer 分析愤怒文本
算法很简单:如果超过 50% 的字符是大写,就说明用户在吼叫,那一定是生气了。否则,Sentimentalyzer 就认为用户心情还不错。
项目结构
这是我的示例项目的文件结构:
main.go <- source for my web app, Sentimentalyzer
Dockerfile <- defines how to run Sentimentalyzer in a Docker container
e2e/ <- folder that contains all the files for my end-to-end tests
cypress.json <- Cypress configuration
docker-compose.yml <- glue that binds together my app container with the Cypress container
integration/
spec.js <- defines the end-to-end test for Sentimentalyzer
所有生产逻辑都在根目录下,而所有端到端测试代码都在 e2e 文件夹中。
在本地运行 Sentimentalyzer
我在这里刻意不展示应用的源代码,以强调一点:你完全可以在不看应用本身实现的情况下编写 Cypress 测试。Sentimentalyzer 碰巧是用 Go 写的,但即使我用 Python 或 Angular 实现,测试也是一样的。如果你好奇,源代码在 GitHub 上。
要在本地体验 Sentimentalyzer,请运行以下命令:
git clone https://github.com/mtlynch/hello-world-cypress.git
cd hello-world-cypress
docker build --tag sentimentalyzer .
docker run \
--interactive \
--tty \
--env PORT=8123 \
--publish 8123:8123 \
sentimentalyzer
上述命令会在你的本地机器上启动一个 Sentimentalyzer 服务,地址为 http://localhost:8123。
既然已经能在 Docker 容器中运行我的应用,现在就可以用 Cypress 为它创建端到端测试了。
创建端到端测试
要编写你的第一个 Cypress 端到端测试,只需要三个文件:
cypress.jsondocker-compose.ymlintegration/spec.js
cypress.json
这个文件用于指定 Cypress 的配置选项:
{
"pluginsFile": false,
"supportFile": false
}
这些设置没什么特别的,我将它们设为 false 只是为了防止 Cypress 自动生成不必要的辅助文件。
docker-compose.yml
这个文件为 Sentimentalyzer 和 Cypress 各定义了一个 Docker 容器,并让它们能够互相通信:
version: "3.2"
services:
sentimentalyzer:
build: ../
environment:
- PORT=8123
cypress:
image: "cypress/included:4.4.0"
depends_on:
- sentimentalyzer
environment:
- CYPRESS_baseUrl=http://sentimentalyzer:8123
working_dir: /e2e
volumes:
- ./:/e2e
有几行值得特别说明:
image: "cypress/included:4.4.0"
cypress/included 是 Cypress Docker 镜像的一个系列,镜像本身已预装了 Cypress。像 cypress/base 和 cypress/browsers 这样的其他系列,则假定由使用者在运行时自行安装 Cypress。使用 cypress/included 镜像可以确保容器一启动就立即执行测试。
depends_on:
- sentimentalyzer
depends_on 这一配置确保在 Cypress 开始执行测试之前,Sentimentalyzer 已启动并运行。
environment:
- CYPRESS_baseUrl=http://sentimentalyzer:8123
环境变量 CYPRESS_baseUrl 告诉 Cypress 在哪里可以访问到 Sentimentalyzer。由于 Cypress 和 Sentimentalyzer 运行在同一个 Docker Compose 配置中,Cypress 可以直接使用容器名(sentimentalyzer)作为主机名向其发送网络请求。
working_dir: /e2e
volumes:
- ./:/e2e
最后,我利用 Docker 的卷挂载功能,让 Cypress 容器共享宿主机的部分文件系统。
宿主机上 ./e2e 目录中的所有内容,都会出现在容器内的 /e2e 路径下。这样一来,Cypress 在运行过程中生成的日志、截图或视频,无需手动从容器拷贝到宿主机,就能立刻在宿主机上查看。以这种方式挂载宿主机卷,还能让你轻松地修改并重新运行测试,而无需重新构建整个 Docker 镜像。
working_dir 这一行则确保 Cypress 将 /e2e 目录视为文件系统中的当前工作目录。
integration/spec.js
配置就讲到这里,接下来是更有趣的部分:编写测试。
it("detects angry sentiment", () => {
cy.visit("/analyze");
cy.get("#feelings").type("I REALLY need some COFFEE");
cy.get("form").submit();
cy.get(".results p").should("contain", "You are feeling: Angry");
});
it("detects content sentiment", () => {
cy.visit("/analyze");
cy.get("#feelings").type("I think coffee in the morning is just swell!");
cy.get("form").submit();
cy.get(".results p").should("contain", "You are feeling: Content");
});
即使你不熟悉 Cypress API,它的语义也足够直观,你大概能一眼看懂这些测试。用大白话来说,这两个测试都遵循同样的流程:
- 在浏览器中,导航到 Sentimentalyzer Web 应用的
/analyze路径。 - 找到文本输入框。
- 输入一些文本。
- 提交表单。
- 读取结果。
我来逐行拆解第一个测试:
cy.visit("/analyze");
这一行告诉 Cypress 在浏览器中加载 Sentimentalyzer 的 /analyze 路径。Cypress 会将其与我在上文 docker-compose.yml 中定义的 CYPRESS_baseUrl 环境变量组合起来,因此完整 URL 是 http://sentimentalyzer:8123/analyze。你无法从开发机直接访问这个 URL,但在 Cypress 容器内部它是一个有效地址。
cy.get("#feelings").type("I REALLY need some COFFEE");
接着,我让 Cypress 找到文本框。这很容易,因为该文本框有一个唯一的 ID feelings,所以我用 CSS 选择器语法 #feelings 来定位元素。

type() 函数会让 Cypress 向我指定的输入框中输入文本。
接下来,Cypress 需要提交表单。Cypress 为这一常见操作提供了 submit() 函数。页面上只有一个 <form> 元素,所以用 CSS 选择器 form 就能轻松找到它并提交表单:
cy.get("form").submit();
提交表单后,Cypress 应该会来到 Sentimentalyzer 的结果页。Cypress 需要检查文本 "You are feeling: Angry",但这稍微有点棘手,因为包含该文本的 <p> 标签没有 ID 属性:

我再次使用 CSS 选择器语法,通过指定 class 为 "results" 的 DOM 节点下的 <p> 元素来定位相关文本:
cy.get(".results p").should("contain", "You are feeling: Angry");
contain 断言会验证 <p> 标签是否包含了我预期的文本。
运行测试
一切就绪,现在来看看 Cypress 的实际运行效果。我用一条简单的命令来运行测试:
cd e2e
docker-compose up --exit-code-from cypress
--exit-code-from cypress 标志告诉 Docker Compose 将 Cypress 容器的退出码作为 docker-compose 命令的退出码。这意味着测试通过时该命令的退出码为 0,测试失败时则为非零。这一特性对于通过命令退出码来判断是否成功的构建脚本或持续集成配置非常有用。
整个过程在控制台中看起来是这样的:
Cypress 会为每次测试运行生成视频录制。这是我最喜欢的功能,因为它对诊断测试失败有极大帮助:
Cypress 端到端测试录像(以 1/4 倍速放慢播放)
测试失败截图
上面我展示的是一个通过的测试。当 Cypress 测试失败时会发生什么?它仍会生成测试运行的视频,同时还会输出一张截图,展示失败的断言:

Cypress 在我的测试失败时生成的截图(Cypress 预期看到“Furious”一词,实际却得到了“Angry”)
这解决了我在使用其他工具时遇到的一个主要痛点。Selenium 也支持截图,但只能在断言之前或之后截图。这一限制曾导致令人沮丧的情况:Selenium 提示测试失败,但截图却显示行为正常,原因是在测试失败后浏览器状态又发生了变化。
Cypress 避免了这个问题,因为它的截图与断言是同时发生的。如果测试失败,截图会精确地展示 Cypress 在失败时刻所看到的画面。
将此方案适配到你的 Web 应用
有了这三个文件,你就可以开始对自己的 Web 应用进行端到端测试了。步骤如下:
- 将 e2e 文件夹复制到你的项目中。
- 将
docker-compose.yml中sentimentalyzer部分替换为你应用的 Docker 容器配置。 - 根据你应用的界面流程重写
integration/spec.js。
源代码及更多示例
本演示的完整源码可在 GitHub 上获取:
我还创建了几个分支来演示其他常见的 Cypress 场景:
延伸阅读
本指南对 Cypress 做了基础介绍。如需了解更多高级功能,请查阅 Cypress 官方文档:
更新(2019-05-02):为回应本文,Cypress 团队发布了预装 Cypress 的官方 Docker 镜像。我已更新本教程以集成他们的新镜像。更多关于镜像的细节以及结合使用 Cypress 与 Docker 的技巧,请查看这篇 Cypress 博客文章。
插图由 Loraine Yow 绘制。感谢来自 Cypress 团队的 Gleb Bahmutov 为本文提供的早期反馈。
随机一篇博客
评论
登录后参与讨论