TestCase-现代-Web-测试指南-全-
TestCase 现代 Web 测试指南(全)
原文:
zh.annas-archive.org/md5/325caba52510c529bb0227d26bcc1ed1译者:飞龙
前言
TestCafe 是一个自给自足、免费且开源的端到端测试框架,它将无与伦比的易用性与高级自动化和强大的内置稳定性机制相结合。它可以用来编写快速且可靠的测试。
《使用 TestCafe 的现代 Web 测试》 是一本全面的项目式入门指南,适合那些刚开始接触 TestCafe 的新手。你将在学习核心方法和概念的同时,构建一套端到端测试。
你将学习如何使用 TestCafe 语法编写端到端测试,以及 TestCafe 框架的功能。你将从设置环境一直到最后编写生产就绪的测试。在整个书中,我们将逐步构建一个示例测试集,该测试集将登录网站,验证不同页面上的元素,创建/删除实体,并使用 TestCafe 执行自定义 JavaScript 代码。此外,还将进行几个重构阶段,以展示设置/清理和 PageObjects。虽然这个测试套件相对简单,但它展示了 TestCafe 的一些最显著功能。此外,它还演示了在免费且易于使用的网站上运行测试,并且不需要读者构建和部署自己的服务器或后端服务。
在本书结束时,你将了解如何使用 TestCafe 编写和增强端到端测试,以解决现实世界的问题并交付结果。你还将有一个概念证明,可以向他人展示。
免责声明:
请注意,本书不是由 Developer Express Inc 编写或制作的。Developer Express Inc 与 Packt 无关,本书版权属于 Packt Publishing Pvt. Ltd。
本书面向对象
本书面向希望使用 TestCafe 进行测试自动化的质量保证工程师、测试自动化工程师、测试软件工程师、SDETs 和软件项目经理。全栈软件开发人员和负责创建企业级测试框架的专业人士也会发现本书很有用。需要具备基本的 JavaScript/Node.js、CSS 选择器、HTML 和 Bash 知识。
本书涵盖内容
第一章,为什么选择 TestCafe?,解释了 TestCafe 是什么以及其主要功能。
第二章,探索 TestCafe 的内部机制,深入探讨了 TestCafe 的工作原理以及其内部隐藏的秘密。
第三章,设置环境,介绍了如何设置环境以运行测试。
第四章,使用 TestCafe 构建测试套件,在介绍了 TestCafe 的主要概念并回顾了其工具集之后,解释了如何选择合适的工具并编写测试。
第五章,改进测试,介绍了如何扩展测试并添加设置和清理。
第六章,使用 PageObjects 重构,介绍了如何通过重构使用 PageObjects 使测试更加有效和透明。
第七章,TestCafe 的发现,提供了对接下来内容的快速概述。
为了充分利用本书
以下表格显示了此书的最低软件要求:

如果您正在使用本书的数字版,我们建议您亲自输入代码或通过下一节中提供的 GitHub 仓库访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从www.packt.com的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择支持选项卡。
-
点击代码下载。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
Windows 上的 WinRAR/7-Zip。
-
Mac 上的 Zipeg/iZip/UnRarX。
-
Linux 上的 7-Zip/PeaZip。
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在 https://github.com/PacktPublishing/获取。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:'在您选择的代码编辑器(或 IDE)中打开basic-tests.js,让我们创建一个简单的测试。'
代码块设置如下:
const { Selector } = require('testcafe');
fixture('My first set of tests');
test('My first test', async (t) => { // Your test code });
任何命令行输入或输出都写作如下:
$ cd test-project/
$ mkdir tests
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:'预期结果:'问题已创建'通知应显示:'
注意事项或重要提示
显示如下。
联系我们
我们欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 customercare@packtpub.com 邮箱联系我们。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权邮箱 copyright@packt.com 联系我们,并附上材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评价
请留下您的评价。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,并且我们的作者可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问packt.com。
第一章:第一章: 为什么选择 TestCafe?
TestCafe——一个新兴的软件测试框架,还是你可以在这里用餐的地方?在这里,我们将探讨 TestCafe 是什么以及它的主要特性。技术是什么?你需要了解什么?它是如何与其他工具集成的?在这本书中,我们将看到 TestCafe 的用途,了解其主要特性,并将其与一个知名的行业标准——Selenium 进行比较。
更实际地说,我们将为缺陷跟踪系统开发一组测试。你将学习如何使用 TestCafe 语法和框架特性编写端到端测试。你将从设置环境一直写到编写生产就绪的测试。
在整本书中,我们将逐步构建一组测试样例,这些测试样例将登录到网站,验证不同页面上的元素,创建/删除实体,并使用 TestCafe 执行自定义 JavaScript 代码。此外,还将有几个重构阶段,以展示设置/清理和 PageObjects。
注意
请记住,这本书并不声称是唯一的信息来源——其主要目标是展示一些原创方法,而不是强制执行严格的规则。请随意使用和扩展本书中探索的所有技术。
到本章结束时,我们将对 TestCafe 有一个清晰的认识,以及接下来要做什么——一个将要测试的功能计划。我们还将回顾演示网站,并制定出一组将在后续章节中自动化的测试用例。
总结本章内容,以下主要主题将被涵盖:
-
介绍 TestCafe。
-
探索 TestCafe 的主要特性。
-
比较 TestCafe 和 Selenium。
-
回顾我们将构建的测试项目。
介绍 TestCafe
如果你为一家大型企业公司或一家小型但创新的初创公司工作,并且你的自动化测试需要支持老版本和新版本的浏览器,你绝对应该尝试 TestCafe。就像 Selenium 一样,它是开源的,但你不需要安装任何其他包或额外的 Web 驱动程序。TestCafe 是一个自给自足、免费、端到端测试框架,它将无与伦比的易用性与高级自动化和强大的内置稳定性机制相结合。
它是由 DevExpress(https://github.com/DevExpress)创建的,并在 MIT 许可下开源。TestCafe 可以处理自动化测试过程的各个阶段:
-
在测试前启动应用程序。
-
启动不同的浏览器。
-
运行测试。
-
捕获截图。
-
输出测试结果。
TestCafe 不需要安装任何额外的浏览器插件,并且可以直接在所有主流现代浏览器中工作。与 Selenium 相比,它因其更快、更易用而越来越受欢迎。
现在我们已经快速了解了 TestCafe 是什么,让我们继续概述其主要特性。
探索 TestCafe 的主要特性
现在,让我们更详细地看看 TestCafe 所能提供的一切:
-
TestCafe 运行所需的基本条件是机器上配置的浏览器和 Node.js,因此设置非常简单。
-
TestCafe 可以在无头模式(在 Chrome 或 Firefox 上)下运行测试,无需渲染文档对象模型(DOM)。当在任何持续集成(CI)系统上运行测试时,这个特性非常有用。
-
TestCafe支持所有主要操作系统,包括 Windows,macOS 和 Linux。
-
TestCafe 官方支持的浏览器包括 Google Chrome(稳定版、Beta 版、Dev 版和 Canary 版)、Internet Explorer(11+)、Microsoft Edge(Legacy 版和基于 Chromium 的)、Mozilla Firefox、Safari、Google Chrome 移动版和 Safari 移动版——因此它是跨浏览器的。您可以在
devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#officially-supported-browsers找到支持的浏览器及其别名的完整列表。 -
测试可以编写为最新的JavaScript (ES6+),TypeScript或CoffeeScript格式(我们将在接下来的章节中使用 JavaScript 进行演示)。
-
清晰且灵活的 API,支持PageModel 模式(我们将在第六章,使用 PageObjects 重构)。
-
由于智能断言和自动等待机制(这将在第二章,探索 TestCafe 内部机制)的稳定测试。
-
TestCafe 拥有许多免费的自定义插件:云浏览器提供商和模拟器(SauceLabs,BrowserStack,CrossBrowserTesting 等),框架特定的选择器以与您的框架(React,Angular,Vue,和 Aurelia)中的页面元素进行交互,自定义报告器以获取不同格式的测试结果(TeamCity,Slack,NUnit 和 TimeCafe),IDE 插件以从您最喜欢的 IDE(Visual Studio Code,Webstorm 和 SublimeText)运行测试并查看结果,Cucumber 支持以 Cucumber 语法创建和运行测试,等等,因为 TestCafe 的开发人员和社区成员非常活跃。所有这些都可以使用并且是开源的(https://github.com/DevExpress/testcafe#plugins)。
这些是 TestCafe 开源框架的主要功能。想知道是否有任何不需要编写代码的产品吗?当然有!
介绍 TestCafe Studio
除了开源的 TestCafe 框架之外,还有一个名为 TestCafe Studio 的付费测试录制工具。它建立在 TestCafe 引擎之上,为测试工程师提供记录、运行和更新测试的机会,而无需任何特殊的 JavaScript 知识。这是通过将用户操作从录制转换为可重复的代码来实现的。
除了补充 TestCafe 框架提供的功能外,TestCafe Studio 还通过以下功能更进一步:
-
可视化测试录制器:这允许您在不编写任何代码的情况下创建测试。它记录您在浏览器中与网页的交互,并生成相应的测试(https://docs.devexpress.com/TestCafeStudio/400165/guides/record-tests)。
-
交互式测试编辑器:允许您以全面、可视化的方式查看和编辑测试和钩子(https://docs.devexpress.com/TestCafeStudio/400190/user-interface/test-editor)。
-
自动选择器生成:当您与网页交互或在元素选择器中选中网页元素时,TestCafe Studio 可以生成元素选择器(https://docs.devexpress.com/TestCafeStudio/400407/test-actions/element-selectors#auto-generated-element-selectors)。
-
运行配置管理器:允许您为桌面、无头和移动浏览器创建、修改和删除运行配置(https://docs.devexpress.com/TestCafeStudio/400189/user-interface/run-configurations-dialog)。
-
代码编辑器:允许您编写和修改测试脚本(https://docs.devexpress.com/TestCafeStudio/400181/user-interface/code-editor)。
注意
TestCafe Studio 提供 30 天的免费试用期——您可以在
www.devexpress.com/products/testcafestudio/qa-end-to-end-web-testing.xml找到更多关于此信息。
让我们停下来,喘口气,回顾一下我们迄今为止所学的内容。我们已经对 TestCafe 有了基本的了解,并了解了一系列它所能提供的内容——主要功能、插件以及 TestCafe Studio 测试录制工具。现在,让我们继续概述 TestCafe 如何与 Selenium 竞争。
比较 TestCafe 和 Selenium
TestCafe,在 GitHub 上拥有超过 8,000 颗星,正在赢得测试自动化领域“下一个大热门”的声誉。让我们将这位新挑战者与 Selenium 进行比较——一个拥有近 18k+颗星的重型领导者,它统治了行业超过 15 年。
要开始使用 Selenium 进行自动化,您将需要为所需的编程语言安装 WebDriver 客户端,并为您想要测试运行的每个浏览器安装相应的驱动程序。这听起来可能是一件简单的事情,但仅仅开始测试就是一个耗时的工作,而且远非我们习惯于 Node.js 基础设施中大多数包的运行一个命令的简单场景。
TestCafe 包含了一些如果 TestCafe 是建立在 Selenium 之上则不可能实现的功能,例如生成隔离的测试环境。TestCafe 执行的每个测试都像是在一个新的隐身标签页中启动,因此所有 cookies 和存储都会被清除。这有助于减少测试代码的重复,并在执行时间上提供了显著的节约,因为你不需要在测试之间清除浏览器状态来使它们相互独立。
这还使一个极其有用的功能成为可能——用户角色,它允许你保存不同登录用户的状态,并在任何测试中随时切换(https://devexpress.github.io/testcafe/documentation/guides/advanced-guides/authentication.html)。
内置的自动等待是 TestCafe 中引入的另一个杀手级功能。这意味着 TestCafe 将在运行每个测试动作之前自动等待所有 XHR 请求和页面加载完成,因此你不再需要在代码中编写自定义等待器。
让我们并排比较 TestCafe 和 Selenium:
![图 1.0 - 比较 TestCafe 和 Selenium 的表格
![img/Figure_1.00_B16280.jpg]
图 1.0 - 比较 TestCafe 和 Selenium 的表格
总结我们刚刚学到的内容,Selenium 在支持编程语言的数量上确实提供了优势,但需要大量的调整和扩展才能正常工作。另一方面,TestCafe 只支持 JavaScript、TypeScript 和 CoffeeScript,但提供了更多的舒适性和易用性。
让我们继续我们的探索,并对我们在以下章节中将开发的测试项目进行一些说明。
审查我们将构建的测试项目
既然我们已经熟悉了 TestCafe 的主要功能,让我们考虑如何最好地利用这个测试框架来满足我们的实际需求。
为了制作一组可重用并展示 TestCafe 主要概念的测试,我们需要一个可测试的应用程序。它应该在线可访问,并应具有一些标准功能,例如登录、登出、创建新实体、显示实体、更新实体和删除实体。
我们将使用 Redmine 应用程序来完成这项工作。
选择测试应用程序
所有的上述功能都存在于任何缺陷跟踪系统中。然而,少数公开可用且免费使用的应用程序之一是 Redmine (demo.redmine.org/):
![图 1.1 – Redmine 示例网络门户
![img/Figure_1.01_B16280.jpg]
图 1.2 – Redmine 示例网络门户
图 1.1 – Redmine 示例网络门户
Redmine 是一个基于 Web 的项目管理与问题跟踪工具,于 2006 年 6 月 25 日发布,是用 Ruby on Rails 编写的。它根据GNU 通用公共许可证 (GPL) v2条款开源。Redmine 支持的一些功能包括问题管理(创建、读取、更新和删除)、版本管理、文档管理、新闻、文件、目录、日历、图表、路线图、活动视图以及成员角色和权限管理。
这是一份令人印象深刻的列表,不是吗?它还支持跨平台、跨数据库,并支持 49 种语言。Redmine 完美地结合了问题跟踪和项目管理功能,可以被认为是开源世界中的领先项目管理解决方案。
编写测试用例
由于我们现在有一个用于测试的 Web 应用程序,让我们熟悉它并编写一些测试用例。我们目前不需要执行这些测试,但稍后编写自动化测试时我们将需要它们。
这是 Redmine 登录页面的样子:


图 1.2 – Redmine 登录页面
让我们分解我们需要执行登录操作的动作:
-
点击登录链接。
-
在登录输入框中输入登录详情。
-
在密码输入框中输入密码。
-
点击登录按钮。
登录成功后我们会看到如下内容:

图 1.4 – Redmine 登录后的页面
图 1.3 – Redmine 登录后的页面
为了确认您已正确登录,请检查用户名是否显示在页面右上角。就这样 – 我们的第一个测试用例准备好了!
但甚至在登录之前,我们还需要创建一个新的测试用户。我们将为每次新的测试运行做这件事 – 这是可以接受的,因为 Redmine 示例门户会定期清除所有用户。为了安全起见,对于测试用户的电子邮件,我们将使用临时电子邮件服务之一 – test_user_testcafe_poc{随机数字}@sharklasers.com – 以及密码 – test_user_testcafe_poc。
为了更结构化地编写测试用例,让我们逐一将它们分解成块。
创建新用户
按照以下步骤创建新用户:
-
点击注册链接。
-
填写登录字段。
-
填写密码字段。
-
填写确认字段。
-
填写名字字段。
-
填写姓氏字段。
-
填写电子邮件字段。
-
点击提交按钮。
预期结果是您的账户已激活。您现在可以登录。通知,应如下显示:


图 1.4 – 账户已激活
登录
按照以下步骤进行登录:
-
点击登录链接。
-
填写登录字段。
-
填写密码字段。
-
点击登录按钮。
预期结果是用户名应该在页面右上角显示:

图 1.5 – 显示用户名
注销
按照以下步骤注销:
-
登录。
-
点击注销按钮。
预期结果是登录链接应该显示在页面右上角:

图 1.6 – 显示登录链接
创建新项目
按照以下步骤创建新项目:
-
登录。
-
点击顶部面板中的项目链接。
-
点击新建项目链接。
-
填写名称字段。
-
点击创建按钮。
预期结果是成功创建的通知显示在顶部:

图 1.7 – 成功创建。通知显示
创建新问题
按照以下步骤创建新问题:
-
登录。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击新问题链接。
-
填写名称字段。
-
填写描述字段。
-
将优先级设置为高。
-
点击创建按钮。
预期结果是问题创建成功的通知应该显示:

图 1.8 – 显示问题创建成功。通知
验证问题是否显示在项目页面上
按照以下步骤验证问题是否显示在项目页面上:
-
登录。
-
创建一个新问题。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击问题链接。
预期结果是问题链接应该显示:

图 1.9 – 显示问题链接
更新问题
按照以下步骤更新问题:
-
登录。
-
创建一个新问题。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击问题链接。
-
点击问题链接。
-
点击编辑链接。
-
清除主题字段并填写新的主题。
-
将优先级设置为正常。
-
点击提交按钮。
预期结果是成功更新的通知应该显示:

图 1.10 – 成功更新。通知显示
验证更新后的问题是否显示在项目页面上
按照以下步骤验证更新后的问题是否显示在项目页面上:
-
登录。
-
创建一个新问题。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击问题链接。
预期结果是更新后的问题链接应该显示:

图 1.11 – 更新后的问题链接显示
搜索问题
按照以下步骤搜索问题:
-
登录。
-
创建一个新问题。
-
打开搜索页面。
-
在搜索字段中输入问题的主题。
-
点击提交按钮。
预期结果是问题链接应显示:

图 1.12 – 显示问题链接
删除问题
按照以下步骤删除问题:
-
登录。
-
创建一个新问题。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击问题链接。
-
点击问题链接。
-
点击删除链接。
-
在浏览器模态窗口中确认删除。
预期结果是应显示无数据可显示的通知:

图 1.13 – 显示无数据可显示的通知
上传文件
按照以下步骤上传文件:
-
登录。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击文件链接。
-
点击新建文件链接。
-
设置文件的路径。
-
点击添加按钮。
预期结果是文件链接和 MD5 校验和应显示:

图 1.14 – 显示文件链接和 MD5 校验和
删除文件
按照以下步骤删除文件:
-
登录。
-
上传新文件。
-
点击顶部面板中的项目链接。
-
点击项目链接。
-
点击文件链接。
-
点击垃圾桶图标。
-
在浏览器模态窗口中确认删除。
预期结果是文件链接和 MD5 校验和不应显示:

图 1.15 – 文件链接和 MD5 校验和未显示
虽然这个测试套件相对简单构建,但它展示了 TestCafe 的一些最显著特性。此外,它演示了在免费且易于使用的网站上运行测试,并且不需要你构建和部署自己的服务器或后端服务。
摘要
在本章中,我们回顾了 TestCafe 是什么以及其主要特性。在比较 TestCafe 与其旧的和经典的竞争对手 – Selenium – 时,我们观察了两个框架的优缺点。Selenium 与 TestCafe 之间的主要区别在于 Selenium 更为重量级,因为它通过与浏览器进程本身交互,通过 Selenium 服务器运行代码,而 TestCafe 在其中插入了一个代理,该代理重写每个 URL 并将测试脚本注入浏览器。它作为一个 Node.js 进程运行,可以在 Node.js 和浏览器环境中执行操作。TestCafe 的主要目标是提供一个现代工具,以减轻主要的测试自动化痛点,并提供一种方便的方式来设置、维护和创建新的测试。
我们也经历了测试项目,并制定了一个计划,即针对即将到来的自动化,哪些测试用例应该被覆盖。
在下一章中,我们将更深入地探讨,看看 TestCafe 在底层是如何运行的,包括它有哪些 API 以及内置的等待函数如何帮助我们将测试用例转换成快速且可靠的自动化测试套件。
第二章:第二章:探索 TestCafe 的内部机制
本章的主要目标是学习 TestCafe 是如何在内部工作的,以及它如何被用于测试自动化来覆盖网站和门户的不同功能。我们将熟悉 TestCafe 的架构、其 API 和自定义客户端代码。
这些主题将使我们能够了解 TestCafe 提供的哪些主要方法和函数,以及如何调用它们。
在本章中,我们将涵盖以下主要内容:
-
探索 TestCafe 架构。
-
了解 TestCafe API。
-
执行自定义客户端代码。
探索 TestCafe 架构
从时间的开始,端到端 Web 测试框架一直依赖于外部驱动程序来在真实浏览器中模拟用户操作。然而,这种方法有几个缺点:
-
第三方依赖和有限支持的浏览器数量:你必须为每个测试环境(有时甚至为每个测试运行)下载、安装、配置和更新额外的驱动程序或库。除此之外,你只能使用每个驱动程序支持的浏览器。
-
缺乏灵活性:旧工具无法直接在测试页面上操作。只要测试代码不干扰应用程序代码,直接在测试页面上操作可以使工具执行许多额外的场景和解决方案。例如,这样它可以添加和删除样式或更改测试页面上任何元素的可见性。
-
代码重复:传统的测试框架在整个测试运行期间使用相同的浏览器实例,从测试到测试保持被测试的 Web 应用程序状态(并在 cookies 和存储中保持相同的值)。因此,端到端测试在测试之间清除 Web 应用程序状态以避免干扰时,有大量的重复代码。
然而,TestCafe 为每个这些问题都提供了一个解决方案。
TestCafe 架构背后的核心思想是用户不需要任何外部驱动程序来运行端到端浏览器测试。相反,所有模拟用户动作的测试脚本都可以从页面本身执行。这使得真正的跨平台和跨浏览器方法成为可能,因为测试将能够在任何具有现代浏览器的设备上运行!
每个测试执行完成后,TestCafe 都会清除浏览器状态:它会删除 cookies,清除localStorage和sessionStorage,并重新加载页面。如果你并行启动多个测试,TestCafe 会在独立的服务器端上下文中执行每个测试运行,以防止服务器端冲突。
TestCafe 的执行可以分为两个部分:
-
服务器端(在 Node.js 进程中)。
-
客户端(在浏览器中)。
让我们来看看这些部分的每一个。
服务器端
测试代码在服务器端的 Node.js 环境中执行。这使得 TestCafe 能够利用独立服务器端代码的优点,包括在测试之前启动测试的 Web 应用程序服务器,以及增强对测试环境和测试执行的掌控。
在 Node.js 中执行测试代码提供了许多优点:
-
可以在测试中完成数据库准备和应用程序的启动。
-
测试可以访问服务器的文件系统,因此你可以读取测试所需的数据或创建文件。
-
测试可以使用 Node.js 的所有最新语法功能。此外,你还可以包含并利用任何 Node.js 第三方包。
-
由于测试逻辑与自动化脚本分离,提高了稳定性和执行速度。
由于 Node.js 代码在服务器上执行,它无法直接访问页面或浏览器的文档对象模型 (DOM),但这由具有访问 DOM 并在浏览器上下文中执行的定制客户端函数处理。
客户端
TestCafe 自动化脚本旨在模拟任何测试页面上用户的行为。它们的主要目标是让你能够编写高级跨浏览器测试,因此元素聚焦、触发事件和处理属性的方式与真实人类在浏览器中操作的方式相同。
模拟用户活动的脚本(TestCafe 内部脚本)在浏览器中测试页面的客户端上运行。这使得 TestCafe 能够利用浏览器脚本的优点,包括内置的智能等待、移动测试和用户角色。为了使客户端代码在浏览器中工作,TestCafe 在服务器上代理测试页面并将脚本注入其代码中。这种方法也被称为反向代理。当你运行 TestCafe 测试时,浏览器地址栏显示的 URL 前缀有一些数字——这是因为 TestCafe 使用其自己的开源 URL 重写代理 (github.com/DevExpress/testcafe-hammerhead) 并代理测试页面。
当你使用 TestCafe 运行测试时,反向代理会自动在你的计算机上本地启动。它将自动化脚本注入测试页面,因此页面代码或与之通信的资源都无法知道页面已被修改。换句话说,当 TestCafe 代理测试页面时,它会添加自动化脚本并将测试页面上所有 URL 重写以指向代理:

Figure 2.1 – TestCafe 反向代理在用户的浏览器和 Web 服务器之间
当浏览器引用这些新的、重写的 URL 时,原始资源也会以相同的方式代理和增强。TestCafe 还模拟浏览器 API 以将自动化脚本与页面其余代码分离。代理机制绝对安全 – 它保证页面看起来像是在原始 URL 上托管,即使是测试代码也是如此。
在本节中,我们回顾了 TestCafe 从服务器和客户端操作的方式。我们还了解了这种架构的主要优势,包括在测试之前预览应用程序的可能性、扩展对测试环境的控制、代理和注入脚本,这使智能等待、移动测试和用户角色成为可能,我们将在稍后讨论这些内容。
TestCafe 支持 JavaScript – 这是网页开发中最受欢迎的编程语言 – 这允许大多数用户使用他们现有的编码技能,并最小化了新手的学习曲线。除此之外,它清晰的 API 使测试易于创建、阅读和维护。因此,让我们看看 TestCafe 提供了哪些方法。
了解 TestCafe API
由于服务器端代码在 Node.js 中运行,因此测试应该用 JavaScript(TypeScript 和 CoffeeScript 也受支持,但最终一切都应该转换为 JavaScript)编写。
TestCafe 使用一个简约的 API,它提供不到几十个方法,这些方法随后被转换成页面上的用户操作。由于我们的测试将使用 TestCafe API 方法与页面交互,让我们回顾 TestCafe 支持的主要交互组:
-
元素选择。
-
操作。
-
断言。
-
用户角色。
让我们更详细地了解这些交互中的每一个。
元素选择
TestCafe 使用一个具有内置等待的高级机制来定位目标元素以执行操作或断言。要执行操作(例如点击、悬停、输入等)或进行断言,您首先应识别目标页面元素。这就像指定一个标准的 CSS 选择器一样简单。对于更复杂的情况,您可以链式调用方法(例如,例如,通过类名获取一个元素,然后获取其第二个子元素,最后获取其第三个兄弟元素)。选择器字符串应传递给链式 Selector 构造函数以创建选择器。
例如,您可以点击具有 button-test 类的按钮,如下所示:
const { Selector } = require('testcafe');const buttonTest = Selector('.button-test');
对于更复杂的情况,您可以通过链式选择器遍历 DOM 树:
const { Selector } = require('testcafe');const linkTest = Selector('#block-test') .child('a') .withAttribute('href', 'https://test-site.com/main.html') .withText('Second link');
这个选择器链所做的是以下内容:
-
选择具有
block-testid 的元素。 -
选择其子元素。
-
通过
a标签过滤它们。 -
选择具有包含
https://test-site.com/main.html的href属性的元素。 -
选择包含
Second link文本的元素。注意
如果一个选择器匹配多个元素,后续的方法将返回所有匹配的元素的结果。
TestCafe 提供了多种方法来搜索相对于所选元素的位置的元素(请注意,所有这些方法都应该以 Selector(cssSelector) 开头)。大多数这些方法接受 index 作为参数,它应该是一个基于零的数字(0 将是集合中最接近的相对元素)。如果数字是负数,则从匹配集的末尾开始计数。以下是一些方法:
-
.find(cssSelector): 在匹配集中查找所有节点的后代节点,并使用 CSS 选择器进行筛选(CSS 选择器应该是一个字符串)(devexpress.github.io/testcafe/documentation/reference/test-api/selector/find.html). -
.parent(index): 查找匹配集中所有节点之父元素(集合中的第一个元素是最接近的父元素)(devexpress.github.io/testcafe/documentation/reference/test-api/selector/parent.html). -
.child(index): 查找匹配集中所有节点的子元素 (devexpress.github.io/testcafe/documentation/reference/test-api/selector/child.html). -
.sibling(index): 查找匹配集中所有节点的兄弟元素 (devexpress.github.io/testcafe/documentation/reference/test-api/selector/sibling.html). -
.nextSibling(index): 查找匹配集中所有节点的后续兄弟元素 (devexpress.github.io/testcafe/documentation/reference/test-api/selector/nextsibling.html). -
.prevSibling(index): 查找匹配集中所有节点的前一个兄弟元素,并通过索引进行筛选 (devexpress.github.io/testcafe/documentation/reference/test-api/selector/prevsibling.html).
现在,让我们看看从选择器中筛选元素的方法。与之前一样,所有这些方法都应该以 Selector(cssSelector) 开头。以下是一些方法:
-
.nth(index): 选择匹配集中指定索引的元素。在这里,index参数应该是一个基于零的数字(0 将是集合中最接近的相对元素)。如果它是负数,则从匹配集的末尾开始计数(devexpress.github.io/testcafe/documentation/reference/test-api/selector/nth.html). -
.withText(text): 选择包含指定文本的元素。在这里,text是元素的文本内容(text参数是一个区分大小写的字符串)或一个应与元素文本匹配的 正则表达式(RegExp)(devexpress.github.io/testcafe/documentation/reference/test-api/selector/withtext.html). -
.withExactText(text): 选择其文本内容严格匹配指定文本的元素。在这里,text是元素的文本内容(text参数是一个区分大小写的字符串)(devexpress.github.io/testcafe/documentation/reference/test-api/selector/withexacttext.html). -
.withAttribute(attrName[, attrValue]): 选择包含指定属性的元素。在这里,attrName可以是一个区分大小写的字符串或一个RegExp,并且可选的attrValue也可以是一个区分大小写的字符串或一个RegExp(devexpress.github.io/testcafe/documentation/reference/test-api/selector/withattribute.html). -
.filterVisible(): 选择不具有display: none;或visibility: hidden;CSS 属性并且具有非零宽度和高度的元素 (devexpress.github.io/testcafe/documentation/reference/test-api/selector/filtervisible.html). -
.filterHidden(): 选择具有display: none;或visibility: hidden;CSS 属性,或宽度或高度为零的元素 (devexpress.github.io/testcafe/documentation/reference/test-api/selector/filterhidden.html). -
.filter(cssSelector): 选择与 CSS 选择器匹配的元素(CSS 选择器应是一个用于过滤子元素的字符串)。此外,您还可以提供filterFn(一个用于过滤元素的函数谓词)和可选的dependencies(一个包含函数、变量或传递给filterFn函数的对象)(devexpress.github.io/testcafe/documentation/reference/test-api/selector/filter.html).
当执行选择器时,TestCafe 将等待目标节点出现在页面上,直到选择器超时时间到期。以下情况下,您可以指定超时时间(以毫秒为单位):
-
.testcaferc.json配置文件中的selectorTimeout配置选项 (devexpress.github.io/testcafe/documentation/reference/configuration-file.html). -
--selector-timeout命令行选项 (devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--selector-timeout-ms). -
Selector(devexpress.github.io/testcafe/documentation/reference/test-api/selector/constructor.html#optionstimeout) 用于设置任何特定元素的超时。
在超时期间,选择器会重新运行,直到返回一个 DOM 元素或超时。如果 TestCafe 在 DOM 中找不到相应的节点,则测试会被标记为失败。
动作
TestCafe API 提供了一套动作方法来与页面交互(例如点击、输入、选择文本、悬停等)。您可以将它们依次以链式方式调用。所有这些方法都应该以 t 开头,因为它们是测试控制器对象的方法 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/)。此外,selector 可以是一个字符串、选择器、DOM 节点、函数或 Promise;并且您可以可选地使用 options,它是一个包含动作补充参数的选项集的对象(除非另有说明)。以下是所有主要动作方法:
-
.click(selector[, options]): 在页面上点击一个元素 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/click.html). -
.doubleClick(selector[, options]): 在页面上双击一个元素 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/doubleclick.html). -
.rightClick(selector[, options]): 在页面上右击一个元素 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/rightclick.html). -
.pressKey(keys[, options]): 按下指定的键盘键。在这里,keys是要按下的键和键组合的序列 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/presskey.html). -
.navigateTo(url): 导航到指定的 URL。在这里,url是要导航到的 URL 字符串(可以是相对于当前页面的绝对路径或相对路径)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/navigateto.html). -
.typeText(selector, text[, options]): 将指定的文本输入到输入元素中。在这里,text是要输入到指定网页元素的文本字符串 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/typetext.html). -
.selectText(selector[, startPos][, endPos][, options]): 在不同类型的输入元素中选择文本。在这里,startPos是选择开始位置的数字(基于零的整数,默认为 0)。可选的endPos是选择结束位置的数字(基于零的整数;默认等于可见文本内容的长度)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/selecttext.html). -
.hover(selector[, options]): 将鼠标指针悬停在网页元素上 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/hover.html). -
.drag(selector, dragOffsetX, dragOffsetY[, options]): 将元素拖动到指定的偏移位置。在这里,dragOffsetX是从鼠标指针原始位置到放下坐标的 X 偏移像素数,而dragOffsetY是从鼠标指针原始位置到放下坐标的 Y 偏移像素数 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/drag.html). -
.dragToElement(selector, destinationSelector[, options]): 将元素拖动到另一个网页元素上。在这里,destinationSelector应该标识将成为放下位置的网页元素 (devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/dragtoelement.html). -
.setFilesToUpload(selector, filePath): 将文件路径添加到指定的文件上传输入中。在这里,filePath是一个字符串或一个包含上传文件路径(或多个路径,如果是数组)的数组。相对路径相对于测试文件所在的文件夹解析(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/setfilestoupload.html)。 -
.clearUpload(selector): 从指定的文件上传输入中删除所有文件路径(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/clearupload.html)。 -
.takeScreenshot([options]): 对整个页面进行截图。可选的options对象可以包含以下属性:包含截图文件相对路径和名称的path字符串或一个指定是否捕获整个页面的fullPage布尔值(默认为 false),包括由于溢出而不可见的页面内容(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/takescreenshot.html)。 -
.takeElementScreenshot(selector[, path][, options]): 对指定的网页元素进行截图。在这里,path(一个可选参数)是一个字符串,包含截图文件的相对路径和名称(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/takeelementscreenshot.html)。 -
.switchToIframe(selector): 将测试的浏览上下文切换到指定的<iframe>(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/switchtoiframe.html)。 -
.switchToMainWindow(): 将测试的浏览上下文从<iframe>切换回主窗口(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/switchtomainwindow.html)。 -
.setNativeDialogHandler(fn(type, text, url)[, options]): 指定一个处理函数来处理测试运行期间触发的原生对话框。在这里,fn(type, text, url)可以是一个函数或客户端函数,每当触发原生对话框时都会被调用(null用于删除原生对话框处理函数)。处理函数可以利用三个参数:type,它是一个字符串,表示原生对话框的类型(confirm、alert、prompt或beforeunload);text,它是一个字符串,表示对话框的消息文本;以及url,它是一个字符串,表示触发对话框的页面的 URL(用于检查对话框是否从主窗口或<iframe>中调用)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/setnativedialoghandler.html)。 -
.getNativeDialogHistory(): 提供已触发的原生对话框的历史记录(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/getnativedialoghistory.html)。 -
.resizeWindow(width, height): 将窗口调整大小以适应提供的宽度和高度,其中width是新宽度的值(以像素为单位),height是新高度的值(以像素为单位)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/resizewindow.html)。 -
.resizeWindowToFitDevice(deviceName[, options]): 将窗口调整大小以适应指定移动设备的屏幕,其中deviceName是设备名称的字符串(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/resizewindowtofitdevice.html)。 -
.maximizeWindow(): 最大化浏览器窗口(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/maximizewindow.html)。 -
.wait(timeout): 暂停测试执行指定的时间。在这里,timeout是暂停时间的长度(以毫秒为单位)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/wait.html)。
断言
TestCafe 允许您验证元素、页面属性和参数(等于、包含、大于、匹配等)。要编写断言,请使用测试控制器的t.expect方法,后跟接受预期值和可选参数的断言方法;message是断言消息字符串,如果测试失败,则显示在报告中,而options是一个包含断言补充参数的选项对象。以下是 TestCafe 中所有内置的断言方法:
-
.expect(actual).eql(expected[, message][, options]): 验证actual值是否等于expected值。在这里,actual是任何类型的比较值,而expected是任何类型的预期值(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/eql.html)。 -
.expect(actual).notEql(expected[, message][, options]): 验证actual值是否不等于expected值。在这里,actual是任何类型的比较值,而expected是任何类型的预期值,它不应等于actual(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/noteql.html)。 -
.expect(actual).ok([message][, options]): 验证actual值是否为true。在这里,actual是在断言中测试的任何类型的值(如果实际值为true,则断言将通过)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/ok.html)。 -
.expect(actual).notOk([message][, options]): 验证actual值是否为false。在这里,actual是在断言中测试的任何类型的值(如果实际值为false,则断言将通过)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/notok.html)。 -
.expect(actual).contains(expected[, message][, options]): 验证actual值是否包含expected值。在这里,actual是任何类型的比较值,而expected是任何类型的预期值(如果实际值包含预期值,则断言将通过)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/contains.html)。 -
.expect(actual).notContains(expected[, message][, options]): 验证actual值是否包含expected值。在这里,actual是任何类型的比较值,而expected是任何类型的预期值(如果实际值不包含预期值,则断言通过)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/notcontains.html)。 -
.expect(actual).typeOf(typeName[, message][, options]): 断言actual值的类型是typeName。在这里,actual是任何类型的比较值,而typeName是实际值的预期类型字符串(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/typeof.html)。 -
.expect(actual).notTypeOf(typeName[, message][, options]): 断言actual值的类型不是typeName。在这里,actual是任何类型的比较值,而typeName是导致断言失败的实际值类型的字符串(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/nottypeof.html)。 -
.expect(actual).gt(expected[, message][, options]): 验证actual值是否大于expected值。在这里,actual是断言中测试的数字(如果实际值大于预期值,则断言通过)而expected是任何类型的预期值(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/gt.html)。 -
.expect(actual).gte(expected[, message][, options]): 验证actual值是否大于或等于expected值。在这里,actual是断言中测试的数字(如果实际值大于或等于预期值,则断言通过),而expected是任何类型的预期值(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/gte.html)。 -
.expect(actual).lt(expected[, message][, options]): 验证actual值是否小于expected值。在这里,actual是断言中测试的数字(如果实际值小于预期值,则断言通过)而expected是任何类型的预期值(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/lt.html)。 -
.expect(actual).lte(expected[, message][, options]): 验证actual值是否小于或等于expected值。在这里,actual是断言中测试的数字(如果实际值小于或等于预期值,则断言将通过)而expected是任何类型的预期值(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/lte.html)。 -
.expect(actual).within(start, finish[, message][, options]): 验证actual值是否在从开始到结束的指定范围内(边界是包含的)。在这里,actual是一个数字,start是下限的数字(包含),而finish是上限的数字(包含)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/within.html)。 -
.expect(actual).notWithin(start, finish[, message][, options]): 验证actual值是否不在从开始到结束的指定范围内(边界是包含的)。在这里,actual是一个数字,start是下限的数字(包含),而finish是上限的数字(包含)(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/notwithin.html)。 -
.expect(actual).match(re[, message][, options]): 验证actual值是否与re正则表达式匹配。在这里,actual是任何类型的比较值,而re是预期匹配实际值的正则表达式(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/match.html)。 -
.expect(actual).notMatch(re[, message][, options]): 验证actual值是否不匹配re正则表达式。在这里,actual是任何类型的比较值,而re是预期不匹配实际值的正则表达式(devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/notmatch.html)。
用户角色
TestCafe 具有内置的用户角色机制,用于模拟用户登录网站的行为。它还保存每个用户的登录状态,这些状态可以在测试的任何部分重复使用,以在用户账户之间切换。这种方法提供了访问一些独特功能:
-
在同一会话期间切换到之前使用的角色时,登录操作不会重复。例如,如果您在
beforeEach钩子中激活一个角色,登录操作将只在第一个测试之前运行一次。所有后续的测试将仅重用现有的认证数据。 -
当您切换角色时,浏览器会自动导航回切换发生的页面,因此不需要为新的角色打开任何 URL(如果需要,此行为可以禁用)。
-
如果在测试期间您登录到多个网站,来自 cookie 和浏览器存储的认证数据将保存在活动角色中。当在同一测试中切换回此角色时,您将自动登录到所有网站。
-
一个匿名内置角色,可以注销所有账户。
让我们看看创建和使用角色的实际示例。
要创建和初始化一个角色,我们需要使用Role构造函数。然后,将登录页面 URL 和登录所需的操作传递给Role。以下代码块展示了这一过程:
const { Role, Selector } = require('testcafe');const regularUser = Role('https://test-site.com/login', async (t) => { await t.typeText('.login', 'TestUser') .typeText('.password', 'testuserpass') .click('#log-in');});const admin = Role('https://test-site.com/login', async (t) => { await t.typeText('.login', 'TestAdmin') .typeText('.password', 'testadminpass') .click('#log-in');});const linkLoggedInUser = Selector('.link-logged-in-user');const linkLoggedInAdmin = Selector('.link-logged-in-admin');fixture('My first test Fixture').page('https://test-site.com');test('Test login with three users', async (t) => { await t.useRole(regularUser) .expect(linkLoggedInUser.exists).ok() .useRole(admin) .expect(linkLoggedInUser.exists).notOk() .expect(linkLoggedInAdmin.exists).ok() .useRole(Role.anonymous()) .expect(linkLoggedInUser.exists).notOk() .expect(linkLoggedInAdmin.exists).notOk();});
在创建所有必需的角色之后,您可以在任何时候切换它们;角色在测试和固定装置之间共享。角色甚至可以创建在单独的文件中,然后在任何引用(需要或导入)此文件的测试固定装置中使用。
总结来说,在本节中,我们回顾了 TestCafe API 及其提供的主要方法。我们还学习了如何选择元素、进行断言以及利用用户角色在不同账户之间切换。现在,让我们看看如何在 TestCafe 中执行自定义客户端代码,以获得对浏览器的更多控制。
执行自定义客户端代码
使用 TestCafe,您可以创建可以在客户端(在浏览器中)运行的客户端函数,并返回任何可序列化的值。例如,您可以获取当前页面的 URL,设置 cookie,甚至操作页面上的任何元素。
在某些复杂场景中,TestCafe 帮助您编写在测试页面上执行的代码。以下是一些可以使用自定义客户端代码完成的任务示例:
-
从网页获取元素以进行进一步操作。TestCafe 允许您根据返回 DOM 节点的客户端代码创建选择器。您可以在服务器端测试中编写此代码,TestCafe 将在需要定位元素时在浏览器中运行这些函数:
const { Selector } = require('testcafe');const testElement = Selector(() => { return document.querySelector('.test-class-name');});await t.click(testElement); -
从客户端函数获取任何可序列化对象的数据,这些对象来自客户端(包括可以转换为 JSON 的任何对象)。与选择器不同,测试代码可以访问客户端函数返回的对象。通常,从客户端函数获取的数据用于断言不同的页面参数。以下是一个获取和验证页面 URL 的示例:
const { ClientFunction } = require('testcafe');const getPageUrl = ClientFunction(() => { return window.location.href;});await t.expect(getPageUrl).eql('https://test-site.com'); -
将自定义代码注入到测试页面中。注入的脚本可以用来添加辅助函数或模拟浏览器 API:
fixture('My second test Fixture') .page('https://test-site.com') .clientScripts( 'assets/jquery-latest.js', 'scripts/location-mock.js' );注意
建议您避免使用自定义客户端代码更改 DOM。一个经验法则是仅使用客户端代码来探索页面,查找并返回信息给服务器。
您可以在以下链接中找到更多客户端脚本和注入的示例:
-
devexpress.github.io/testcafe/documentation/guides/basic-guides/obtain-client-side-info.html. -
devexpress.github.io/testcafe/documentation/guides/advanced-guides/inject-client-scripts.html.
正如我们刚刚发现的,TestCafe 客户端函数对于不同的浏览器操作和获取测试中需要验证的额外数据非常有用。
摘要
在本章中,我们了解了 TestCafe 的工作原理。我们熟悉了 TestCafe 的架构,看到了它在客户端和服务器端的性能表现,并学习了选择元素、动作、断言、角色和自定义客户端代码的策略。
所有这些内容都将在接下来的章节中用于编写我们自己的端到端测试套件。除此之外,你还可以随时将本章作为参考,搜索任何特定的方法或断言,并查看其调用方式和功能。
现在,让我们从 TestCafe 的主要方法和函数转向更实用的方面,比如为我们的未来自动化测试设置测试环境。
第三章:第三章:设置环境
本章的主要学习目标是熟悉使用 TestCafe 设置测试环境以编写端到端测试。你将学习如何设置 Node.js 环境(包括 TestCafe 本身),创建一个基本的配置文件来运行测试,以及如何构建测试代码以遵循最佳实践。
这尤其重要,因为在现实生活中,每个新的项目/存储库通常都需要设置测试基础设施以防止回归并保持代码质量。
总结来说,本章将涵盖以下主要主题:
-
设置测试项目环境。
-
创建测试项目配置文件。
-
构建测试代码。
技术要求
本章的所有代码示例都可以在 GitHub 上找到,地址为 github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/tree/master/ch3。
设置测试项目环境
现在正确设置环境非常重要,因为我们将在本章的剩余部分以及整本书的结尾使用它。这样做还将帮助您了解 Node.js 如何处理不同的包以及如何启动几乎任何基于 JavaScript/Node.js 的测试框架。我们将设置过程分为两个部分:
-
安装 Node.js。
-
安装 TestCafe。
因此,让我们从头开始,从安装 Node.js 开始整个过程。
安装 Node.js
JavaScript 是一种客户端编程语言,主要处理前端,这意味着它通常由打开您网站或 web 应用的每个用户的浏览器处理。Node.js 作为一种 JavaScript 运行时环境被开发出来,以提供将 JavaScript 作为服务器端后端语言的能力。
为了启动几乎所有用 JavaScript 编写的开发工具,您需要使用 Node.js 和 node_modules 文件夹。
Node.js 可用于多种操作系统,包括 macOS、Ubuntu/Linux 和 Windows。安装 Node.js 和 npm 的最简单方法是按照以下步骤操作:
-
选择 长期支持 (LTS) 版本。
-
选择您的操作系统。
-
下载安装文件并运行它。
另一种稍微复杂但可重用的方法是,通过 Node 版本管理器 (nvm – github.com/nvm-sh/nvm) 或 n (github.com/tj/n) 安装 Node.js。版本管理器允许您同时安装多个 Node.js 版本,并且可以随时在它们之间切换,这在测试开发期间非常有用。
安装完成后,您可以通过打开任何外壳(例如,终端或 PowerShell)并执行以下命令来检查 Node.js 和 npm 是否正常工作:
$ node -v
$ npm -v
这应该会分别输出 Node.js 和 npm 的版本号。
安装 TestCafe
由于我们已经安装了 Node.js 和 npm,让我们继续安装 TestCafe。它可以从 npm 本地安装(从您的项目文件夹运行)或全局安装(从任何位置运行)。
本地安装 TestCafe
要将 TestCafe 本地安装到您的项目目录并保存到依赖项列表中,请打开任何 shell,转到您的项目文件夹,并执行以下两个命令:
$ npm init --yes
$ npm install testcafe --save-dev
第一个命令将创建一个简单的 package.json 文件来存储所有依赖项。第二个命令将安装 testcafe 包并将其保存到 package.json 中您项目的依赖项列表中。
全局安装 TestCafe
要全局安装 TestCafe,打开任何 shell 并执行以下命令:
$ npm install testcafe --global
这将全局安装 TestCafe,并且可以从任何文件夹访问它。
您可以通过执行以下命令来检查已安装的 testcafe 包的版本:
$ npx testcafe -v --no-install
注意
在 macOS(从 v10.15 Catalina 版本开始),TestCafe 需要屏幕录制权限来执行测试操作并截图和录制视频。当 TestCafe 首次启动测试时,macOS 将要求您允许 TestCafe 浏览器工具进行屏幕录制。转到 系统偏好设置 - 安全性与隐私 - 隐私,并勾选 TestCafe 浏览器工具 以授予权限。当您更新 macOS 或 TestCafe 时,安全权限可能会被清除——在这种情况下,系统将重复请求。因此,当 安全性与隐私 弹出窗口再次打开时,只需取消勾选并重新勾选 TestCafe 浏览器工具 复选框。
现在,由于我们已经安装并准备好了 Node.js、npm 和 TestCafe,让我们继续创建测试项目的配置文件。
创建测试项目配置文件
在本节中,我们将了解如何配置 TestCafe。然而,在审查主要配置选项之前,让我们为一些编码风格标准设定一个约定。
接受代码风格约定
在本书编写代码的过程中,我们将遵循一些简单的规则,例如 .json 文件使用两个空格缩进,.js 文件使用四个空格缩进。我们还将使用分号和单引号。大多数流行的代码编辑器都支持 .editorconfig 配置文件来自动应用这些规则:
root = true [*]indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true max_line_length = 120 [*.json]indent_size = 2
您可以从 github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/.editorconfig 复制我们将要使用的基本配置文件。
探索配置设置
TestCafe 配置设置通常存储在项目根目录下的 .testcaferc.json 文件中。让我们看看可以指定的主要选项:
-
browsers是一个字符串,或一个字符串数组,用于设置要启动测试的一个或多个浏览器。对于任何本地安装的浏览器,例如chrome、firefox、safari、ie、edge或opera,应指定浏览器别名(devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#locally-installed-browsers)。您可以在系统中的任何壳中打开并运行以下命令来查看所有可用的浏览器列表:.testcaferc.json will look like this:{ "browsers": "chrome"}
To run tests in Firefox and Chrome, your test will look like this:{ "browsers": ["firefox", "chrome"]}
To run tests in remote browsers (such as SauceLabs, BrowserStack, CrossBrowserTesting, and so on) with a browser provider plugin, set the browser provider name, together with the browser alias and operating system, as follows:{ "browsers": "saucelabs:Chrome@83.0:Windows 10"}
Postfixes to browser aliases can be used to launch tests in headless mode or to apply Chrome device emulation ([`devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#use-chromium-device-emulation`](https://devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#use-chromium-device-emulation)):{ "browsers": ["firefox:headless", "chrome:emulation:device=iphone X"]}
NoteTestCafe starts Chrome and Firefox with a fresh profile by default, without any extensions or profile settings. If you need to launch a browser with the current user profile, add the `:userProfile` postfix flag after the browser alias. -
src是一个字符串,或一个字符串数组,用于设置从其中启动测试的文件或目录的路径。要从单个文件运行测试,请使用以下代码:{ "src": "tests/login-test.js"}可以使用全局模式来解析一组文件:
{ "src": ["tests/**/*.js", "utils/helpers/"]} -
reporter是一个字符串或一个对象数组,用于设置用于生成测试报告的内置或自定义报告器的名称(devexpress.github.io/testcafe/documentation/guides/concepts/reporters.html)。默认情况下,使用spec报告器。要指定任何其他报告器,例如minimal,请使用以下命令:{ "reporter": "minimal"}可以同时设置多个报告器,但只能有一个报告器可以写入控制台输出(标准输出,或
stdout),所有其他报告器应写入文件:{ "reporter": [ { "name": "minimal" }, { "name": "json", "output": "tests/reports/report.json" }, { "name": "xunit", "output": "tests/reports/report.xml" } ]}您还可以探索和使用来自
www.npmjs.com/search?q=testcafe-reporter的任何可用报告器。 -
screenshots是一个对象,允许您设置截图选项。这些选项包括path,它是一个字符串,表示保存截图的目录;takeOnFails,它是一个布尔值,表示是否在测试失败时捕获截图;pathPattern,它是一个字符串,用于创建相对路径和截图的名称;以及fullPage,它是一个布尔值,表示是否应捕获整个页面截图(包括由于溢出而不可见的任何内容):{ "screenshots": { "path": "tests/screenshots/", "takeOnFails": true, "pathPattern": "${DATE}_${TIME}/test-${TEST_ INDEX}/${USERAGENT}/${FILE_INDEX}.png", "fullPage": true }}注意
请参阅可用于截图和视频的完整占位符路径模式列表,链接为
devexpress.github.io/testcafe/documentation/guides/advanced-guides/screenshots-and-videos.html#path-pattern-placeholders。 -
videoPath是一个字符串,表示保存测试运行视频的目录:{ "videoPath": "tests/videos/"} -
videoOptions是一个对象,允许您设置视频选项。这些选项包括failedOnly,这是一个布尔值,应设置为true以仅对失败的测试进行录制,或设置为false(默认值)以录制所有测试;singleFile,这是一个布尔值,应设置为true以将整个记录保存到单个文件中,或设置为false(默认值)以每个测试保存到单独的文件;以及pathPattern,这是一个字符串,用于自定义模式来组合相对路径和视频文件名:{ "videoOptions": { "failedOnly": true, "singleFile": true, "pathPattern": "${TEST_INDEX}/${USERAGENT}/${FILE_INDEX}.mp4" }} -
videoEncodingOptions是一个对象,用于设置视频编码选项(支持所有FFmpeg库选项,您可以在ffmpeg.org/ffmpeg.html#Options找到这些选项)。例如,让我们设置帧率和视频显示宽高比:{ "videoEncodingOptions": { "r": 24, "aspect": "16:9" }} -
quarantineMode是一个布尔值,用于将失败的测试切换到隔离模式(以重新运行不稳定的测试):{ "quarantineMode": true }如果启用隔离模式,测试运行将遵循以下逻辑:

图 3.1 – 在隔离模式下运行的测试逻辑
-
debugMode是一个布尔值,用于在调试模式下运行测试:{ "debugMode": true }注意
如果启用调试模式,测试执行将在第一个动作或断言之前暂停,以便您可以打开开发者工具并开始调试。为了便于操作,将在页脚显示一个状态栏,显示可用的调试操作:解锁页面、继续和下一步。
-
debugOnFail是一个布尔值,用于在测试失败后自动启用调试模式。如果此选项设置为true(默认设置为false),TestCafe 将在测试失败时暂停,以便您可以查看测试页面,打开开发者工具,并找出失败的原因:{ "debugOnFail": true } -
skipJsErrors是一个布尔值,用于忽略测试网页上的 JavaScript 错误(默认情况下,当这些错误发生时,TestCafe 将停止运行测试,并在输出报告中发布带有堆栈跟踪的错误消息):{ "skipJsErrors": true } -
skipUncaughtErrors是一个布尔值,用于忽略测试网页上的未捕获 JavaScript 错误和未处理的承诺拒绝(默认情况下,当这些错误或承诺拒绝发生时,TestCafe 将停止运行测试,并在输出报告中发布带有堆栈跟踪的错误消息):{ "skipUncaughtErrors": true } -
appCommand是一个字符串,用于在测试开始之前执行指定的 shell 命令。此选项通常用于启动需要运行测试的应用程序(在所有测试执行完毕后,此类应用程序将自动停止):{ "appCommand": "node server.js"} -
appInitDelay是 TestCafe 在启动测试之前等待的时间(以毫秒为单位;默认值为1000)。因此,此延迟用于给使用appCommand选项启动的应用程序一些启动时间:{ "appCommand": "node server.js", "appInitDelay": 5000 } -
concurrency是用于并行运行测试的浏览器实例的数量。TestCafe 将以指定的浏览器实例数量开始,并创建这些实例的池。测试将同时针对此池启动;每个测试将从池中获取第一个空闲的浏览器实例并在其中运行:{ "concurrency": 4 } -
selectorTimeout是选择器在请求检索网页元素节点时的时间(以毫秒为单位;默认值为10000):{ "selectorTimeout": 15000 } -
assertionTimeout是 TestCafe 执行断言请求的时间(以毫秒为单位;默认值为3000)。此超时仅适用于在断言中使用选择器属性或客户端函数作为实际值的情况:{ "assertionTimeout": 5000 } -
pageLoadTimeout是在DOMContentLoaded事件之后 TestCafe 等待window.load事件被触发的时间(以毫秒为单位;默认值为3000)。TestCafe 在window.load事件被触发或超时通过(以先发生者为准)后开始测试:{ "pageLoadTimeout": 10000 } -
speed是测试执行速度(1是最快的,0.01是最慢的;默认值为1)。此选项可用于减慢测试速度:{ "speed": 0.5 }注意
如果在
.testcaferc.json中设置了速度,并且在针对单个操作的测试中也设置了速度,则操作的速度设置将具有更高的优先级,并将覆盖配置文件中设置的速度: -
clientScripts是一个对象、一个对象数组或一个字符串,用于在测试期间注入任何打开的页面中的脚本。此属性通常用于添加客户端模拟函数、模块或辅助脚本。您可以设置content,这是一个包含要注入的 JavaScript 代码的字符串;module,这是一个包含要注入的模块名称的字符串;以及path,这是一个包含要注入的 JavaScript 文件的路径的字符串。这些设置中的任何一个都可以与可选的page设置配对,以设置应注入提供的脚本的特定页面:{ "clientScripts": [ { "content": "Date.prototype.getTimestamp = () => new Date().getTime().toString();" }, { "module": "js-automation-tools" }, { "path": "scripts/helpers.js", "page": "https://test-site.com/page/" } ]} -
port1和port2是范围在0到65535之间的数字,代表一个自定义端口,TestCafe 使用它来启动测试基础设施(如果未设置端口,TestCafe 将自动选择它们):{ "port1": 12340, "port2": 56789 } -
hostname是您计算机的主机名,当您在远程浏览器中运行测试时使用。如果未设置hostname,TestCafe 将使用操作系统的主机名或当前机器的网络 IP 地址:{ "hostname": "host.test-site.com"} -
proxy是用于您本地网络中访问互联网的代理服务器的字符串:{ "proxy": "123.123.123.123:8080"}认证凭据也可以通过代理主机设置:
{ "proxy": "username:password@proxy.test-site.com"} -
proxyBypass是一个字符串(或字符串数组),要求 TestCafe 绕过代理服务器以访问指定的资源:{ "proxyBypass": ["localhost:8080", "internal.corp.test-site.com"]} -
developmentMode是一个布尔值,用于诊断错误(如果您想向 TestCafe 支持报告问题,应将此选项设置为true):{ "developmentMode": true } -
stopOnFirstFail是一个布尔值,用于在任何一个测试失败后立即停止测试运行:{ "stopOnFirstFail": true } -
tsConfigPath是一个字符串,用于使 TestCafe 能够使用自定义 TypeScript 配置文件并设置其位置 (devexpress.github.io/testcafe/documentation/guides/concepts/typescript-and-coffeescript.html#customize-compiler-options)。可以使用相对路径或绝对路径:{ "tsConfigPath": "/Users/john/testcafe/tsconfig.json"}在相对路径的情况下,它们将相对于你运行 TestCafe 的目录进行解析。
-
disablePageCaching是一个布尔值,用于防止浏览器缓存页面内容:{ "disablePageCaching": true }当浏览器在角色代码中打开缓存页面时,
localStorage和sessionStorage的内容将不会被保存。为了在导航后保留存储项,将disablePageCaching设置为true。注意
这里是一个包含所有主要设置的
.testcaferc.json文件的好例子:github.com/DevExpress/testcafe/blob/master/examples/.testcaferc.json。
为测试项目创建基本配置
现在,让我们将本节所学的内容综合起来,通过打开任何外壳(例如,我们将使用带有 Bash 的终端)并执行以下步骤来为我们的测试项目创建一个具有基本配置的文件夹:
-
由于我们已经下载并安装了 Node.js,让我们检查其版本:
$ node -v -
然后,为你的未来测试项目创建一个文件夹:
$ mkdir test-project -
现在,进入那个文件夹,初始化一个基本的
package.json文件以存储所有依赖项:$ cd test-project/ $ npm init --yes -
之后,安装 TestCafe 包并将其保存为开发依赖项:
$ npm install testcafe --save-dev -
作为最终步骤(目前),创建一个
.testcaferc.json配置文件,包含一组最小选项:{ "browsers": "chrome", "src": [ "tests/**/*.js", "tests/**/*.feature" ], "screenshots": { "path": "tests/screenshots/", "takeOnFails": true, "pathPattern": "${DATE}_${TIME}/test-${TEST_INDEX}/${USERAGENT}/${FILE_INDEX}.png" }, "quarantineMode": false, "stopOnFirstFail": true, "skipJsErrors": true, "skipUncaughtErrors": true, "concurrency": 1, "selectorTimeout": 3000, "assertionTimeout": 1000, "pageLoadTimeout": 1000, "disablePageCaching": true }
我们已经在 探索配置设置 部分涵盖了此文件中的选项,因此你可以随时参考它来理解这个例子。
你还可以从 GitHub 上审查和下载此配置文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch3/test-project/.testcaferc.json。
由于我们已经安装了 Node.js 和 TestCafe 并创建了基本配置文件,让我们通过组织测试代码结构来继续设置我们的测试项目。
测试代码的结构化
为了更好地理解测试代码结构组织,让我们将其分为几个部分:固定装置、测试、起始网页、元数据和跳过测试。
固定装置
TestCafe 测试通常被分组到测试套件中,称为固定配置(与 Jasmine 和 Mocha 测试框架中的describe块相同)。任何包含 TestCafe 测试的 JavaScript、TypeScript 或 CoffeeScript 文件都应该包含一个或多个固定配置。固定配置可以使用fixture函数声明,它只接受一个参数——fixtureName,这是一个字符串,表示固定配置(测试集)的名称:
fixture('Name for the set of the tests');
或者,你也可以这样写,不使用括号:
fixture `Name for the set of the tests`;
固定配置基本上是一个包装器,用于指示一组测试的开始。让我们看看这些测试应该如何结构化。
测试
测试通常在fixture声明之后编写。要创建一个测试,请调用test函数,它接受两个参数:
-
testName:一个字符串,表示测试的名称。 -
function:一个包含测试代码的异步函数,它接受一个参数——t,这是一个用于访问所有操作和断言的测试控制器对象。
一个简单的测试通常看起来像这样:
test('Go to the main page', async (t) => { await t.click('#button-main-page'); await t.expect(Selector('#logo-main-page').visible).ok();});
由于 TestCafe 测试是在服务器端执行的,因此你可以使用任何额外的包或模块。此外,在测试内部,你可以执行以下操作:
-
使用测试操作与被测试的网页进行交互。
-
使用选择器和客户端函数来获取有关页面元素状态的信息或从客户端获取其他数据。
-
使用断言来验证页面元素是否具有预期的参数。
现在,让我们看看如何在固定配置中为所有测试指定起始页面。
起始网页
你可以使用fixture.page函数在一个固定配置中设置初始网页,它将成为所有测试的起点:它只接受一个参数——url,这是一个字符串,表示网页的 URL,所有固定配置中的测试都是从该网页开始的:
fixture('Contacts page').page('http://test-site.com/example');test('Test Contact form', async (t) => { // Starts at http://test-site.com/example });
接下来,让我们看看如何为固定配置和测试指定元数据。
元数据
在 TestCafe 中,你还可以为测试提供额外的信息,例如键值元数据。这可以用于过滤测试并在报告中显示这些数据。要定义元数据,请使用fixture.meta和test.meta方法。它们接受两个字符串参数:
-
name:一个字符串,表示元数据条目的名称。 -
value:一个字符串,表示元数据条目的值。
或者,它们可以接受一个参数——metadata,这是一个包含元数据键值对的对象。
这两种设置元数据的方式可以组合使用,看起来是这样的:
fixture('Contacts page') .meta('env', 'production') .meta('fixtureId', 'f0001') .meta({ author: 'John', creationDate: '01.06.2020' });test.meta('testId', 't0001') .meta({ testType: 'fast', testedFeatureVersion: '1.1' }) ('Test Contact form', async (t) => { // Your test code });
固定配置或测试可以通过它们包含的特定元数据值来启动。要按metadata过滤测试,请将filter.testMeta和filter.fixtureMeta属性添加到.testcaferc.json配置文件中:
{ "filter": { "fixtureMeta": { "env": "production", "author": "John" }, "testMeta": { "testType": "fast", "testedFeatureVersion": "1.1" } }}
此配置将仅运行具有metadata的testType属性设置为fast和testedFeatureVersion设置为1.1的测试,以及其固定配置的元数据具有env属性设置为production和author属性设置为John的测试。
您可以使用自定义报告器(devexpress.github.io/testcafe/documentation/guides/extend-testcafe/reporter-plugin.html)来在报告中显示测试用例和测试的元数据。
跳过测试
在 TestCafe 中,您还可以指定在所有其他测试运行时跳过的测试用例或测试。这是通过 fixture.skip 和 test.skip 方法实现的:
fixture.skip('Contacts page');test('Test Contact form', async (t) => { // Your test code });test('Test Review form', async (t) => { // Your test code });fixture('About page');test('Test Reviews block', async (t) => { // Your test code });test.skip('Test More info form', async (t) => { // Your test code });test('Test Our mission block', async (t) => { // Your test code });
在本例中,Contacts 页面测试用例将不会被运行。Test More info form 测试也不会被执行。
另一对有用的方法是 fixture.only 和 test.only。它们用于指定仅启动特定的测试用例或测试用例,其他所有测试用例将被跳过。如果有多个测试用例被标记为 .only,则所有标记为 .only 的测试用例都将被执行:
fixture.only('Contacts page');test('Test Contact form', async (t) => { // Your test code });test('Test Review form', async (t) => { // Your test code });fixture('About page');test('Test Reviews block', async (t) => { // Your test code });test.only('Test More info form', async (t) => { // Your test code });test('Test Our mission block', async (t) => { // Your test code });
在本例中,只有来自 Contacts 页面测试用例和 Test More info form 测试用例将被执行。
摘要
在本章中,我们学习了如何使用 TestCafe 设置编写端到端测试的测试环境。我们安装了 Node.js 和 TestCafe,审查了配置选项,并创建了一个基本的 .testcaferc.json 文件来存储它们。除此之外,我们还了解了几种结构化 TestCafe 代码的技术,包括测试用例、测试、起始网页、元数据和跳过测试。
本章的教训非常重要,因为您将进入任何新开始项目的配置阶段。
现在,我们已经做好了充分的准备,可以开始利用这些知识来编写我们的测试项目的 TestCafe 测试。我们将学习如何创建和调试测试,并在之后立即开始构建一个真实的测试套件。
第四章:第四章:使用 TestCafe 构建测试套件
现在,我们已经了解了 TestCafe 的主要概念并审查了其工具箱,让我们拿起武器并编写一些测试!此处的首要目标将是熟悉如何使用 TestCafe 编写一组端到端测试(测试套件)。这非常重要,因为我们将涵盖的测试技术是通用的,可以重用于编写任何 Web 项目的自动化测试。
总结来说,本章将涵盖以下主要主题:
-
创建测试。
-
调试测试。
-
在测试项目中编写测试项目日志。
-
向测试项目中添加验证。
-
在测试项目中添加自定义代码执行。
-
添加更多测试。
技术要求
正如我们在 第三章**,设置环境 中提到的,在编写本书中的代码时,我们将遵循一些编码约定:.json 文件使用两个空格缩进,.js 文件使用四个空格缩进,使用分号,并使用单引号。我们还将利用 JavaScript ES6+ 语法,包括模板字符串。
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4.
在测试项目中创建测试。
TestCafe 支持使用 JavaScript、TypeScript 或 CoffeeScript 编写的测试,并具有所有现代功能,如箭头函数和 async/await。除此之外,TestCafe 还会在运行测试之前自动将 TypeScript 和 CoffeeScript 代码转换为 JavaScript,因此您无需自己处理。
正如我们最初商定的,在本书中我们将使用 JavaScript 编写测试。
在延续我们之前在 第三章**,设置环境 的努力之后,我们已经在 test-project 文件夹中有了 .testcaferc.json 配置文件。因此,让我们首先打开任何 shell(例如,我们将使用带有 bash 的终端)并遵循以下步骤:
-
前往
test-project文件夹并为我们的测试创建一个文件夹:$ cd test-project/ $ mkdir tests -
现在,请转到该文件夹并创建一个
basic-tests.js文件:$ cd tests/ $ touch basic-tests.js -
在您选择的代码编辑器(或 IDE)中打开
basic-tests.js并让我们创建一个简单的测试。 -
我们首先将包括
testcafe模块:const { Selector } = require('testcafe'); -
然后我们使用
fixture函数声明一个固定测试:const { Selector } = require('testcafe'); fixture('My first set of tests'); -
使用
test函数声明第一个测试:const { Selector } = require('testcafe'); fixture('My first set of tests'); test('My first test', async (t) => { // Your test code }); -
由于我们选择了 Redmine (
demo.redmine.org/) 作为我们的测试项目,请使用page函数将此 URL 设置为'My first set of tests'固定测试的起始页面:const { Selector } = require('testcafe');fixture('My first set of tests') .page('http://demo.redmine.org/');test('My first test', async (t) => { // Your test code });注意
您也可以在 GitHub 上审查和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests1.js.
既然我们现在有一个空的测试结构,让我们运行它并检查输出。
运行测试
我们可以通过在命令行中执行单个命令,指定目标浏览器和文件路径,轻松运行测试:
$ npx testcafe chrome tests/basic-tests.js
Shell 输出将看起来像这样:
![图 4.1:测试运行后的 Shell 输出]
]
图 4.1 – 测试运行后的 Shell 输出
TestCafe 将自动启动所选的浏览器实例,并开始运行测试。正如您在测试输出中看到的那样:“配置文件中的src、browsers选项将被忽略。”这意味着我们已经在.testcaferc.json中指定了我们的默认浏览器和测试的路径(您可以在这里看到它),我们提供的命令行选项已经覆盖了默认设置。
因此,我们现在可以进一步简化测试运行命令:
$ npx testcafe
现在,TestCafe 将仅从.testcaferc.json中获取默认选项,测试运行的结果将与之前相同。我们将在第五章**,改进测试中稍后回顾更多的命令行界面(CLI)设置。
注意
保持运行测试的浏览器处于活动状态,不要最小化浏览器窗口。最小化的浏览器窗口和未激活的标签页往往会切换到资源消耗减少的模式,在这种模式下,测试可能无法正确执行。
执行动作
现在我们来在页面上执行一些动作:
const { Selector } = require('testcafe');fixture('My first set of tests') .page('http://demo.redmine.org/');test('My first test', async (t) => { await t.click('.login');});
注意
您还可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests2.js。
之前的设置包含一个简单的测试,点击t。测试控制器对象让我们可以访问测试运行的 API。在调用测试动作或动作链时,应该使用await关键字等待它们完成。
既然我们已经学会了如何运行基本测试,让我们谈谈如何处理调试和错误。
调试测试
现在我们来看看如何调试我们的测试。我们将分两个部分来回顾:
-
在 TestCafe 中调试测试。
-
在 Chrome 开发者工具中调试测试。
让我们看看。
在 TestCafe 中调试测试
让我们使用上一个示例中的代码,创建一个basic-test-wrong.js文件,并在选择器中稍作修改类名:
const { Selector } = require('testcafe');fixture('My first set of tests') .page('http://demo.redmine.org/');test('My first test', async (t) => { await t.click('.login-wrong');});
注意
您还可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-test-wrong.js。
使用 .login-wrong 类名而不是 .login 来引发 元素未找到 错误。让我们启动我们的测试以确认这一点:
$ npx testcafe chrome tests/basic-test-wrong.js
输出将类似于这样:

Figure 4.2 – 测试运行后带有错误的 Shell 输出
如您所见,TestCafe 输出了错误及其发生的位置。
但在测试失败之前,您如何调试测试呢?为此,TestCafe 提供了 t.debug 方法。让我们将其添加到我们的测试中:
const { Selector } = require('testcafe');fixture('My first set of tests') .page('http://demo.redmine.org/');test('My first test', async (t) => { await t.debug().click('.login-wrong');});
此方法用于暂停测试执行,并使用浏览器的开发者工具进行调试。您将在浏览器窗口的页脚中看到一些按钮,用于导航测试运行流程:
-
解锁页面:允许我们与当前打开的浏览器页面进行交互。
-
恢复:允许我们继续测试运行。
-
下一步操作:执行下一个操作或断言:

Figure 4.3 – TestCafe 调试按钮
输出将类似于这样:

Figure 4.4 – 调试模式下的 Shell 输出
如您所见,TestCafe 输出了调用 t.debug 方法的代码行。
您也可以使用 --debug-mode 标志来运行测试。这将启用调试并在第一次操作或断言之前暂停测试执行:
$ npx testcafe chrome tests/basic-test-wrong.js --debug-mode
或者,您可以使用 --debug-on-fail 标志。当测试失败时,它将暂停测试,并允许您查看测试页面并确定失败的原因:
$ npx testcafe chrome tests/basic-test-wrong.js --debug-on-fail
当您对页面调试满意时,只需点击页脚中的 完成 按钮即可终止测试执行过程。
现在让我们看看如何在 Chrome Dev Tools 中调试测试。
在 Chrome 开发者工具中调试测试
另一种调试测试的有用方法是通过 Chrome 开发者工具中的 Node.js 进行。您需要 Google Chrome 和 Node.js v8 或更高版本来执行所有操作。要使用 Chrome 开发者工具进行调试,请按照以下步骤操作:
-
首先,在测试代码中放置
debugger关键字,以便在您希望进程停止的地方:const { Selector } = require('testcafe');fixture('My first set of tests') .page('http://demo.redmine.org/');test('My first test', async (t) => { debugger; await t.click('.login-wrong');}); -
然后,为了激活 Node.js 调试模式,将
--inspect-brk标志添加到测试运行命令中:$ npx testcafe --inspect-brk chrome tests/basic-test-wrong.js -
打开 Google Chrome 并导航到 chrome://inspect。在 远程目标 部分,找到 Node.js 调试器并点击 检查。Chrome 将启动开发者工具,调试器将在第一行停止测试执行。点击 恢复脚本执行 按钮继续:

Figure 4.5 – Google Chrome 开发者工具调试器
如您所见,测试执行在带有 debugger 关键字的行上暂停,允许您调试代码。
现在,我们已经学会了如何调试测试代码,让我们进一步编写一些针对测试项目的登录测试。
在测试中编写测试项目日志
正如我们在第一章**,为什么选择 TestCafe中讨论的那样,我们需要一个测试用户登录到门户并执行任何进一步的测试。因此,让我们从创建一个带有电子邮件地址的新用户开始——test_user_testcafe_poc{randomDigits}@sharklasers.com——以及密码——test_user_testcafe_poc。
让我们声明以下测试以在basic-tests.js中注册新用户:
const { Selector } = require('testcafe');fixture('Redmine log in tests') .page('http://demo.redmine.org/');test('Create a new user', async (t) => {
测试将执行以下操作:
-
测试点击了注册链接:
await t.click('.register') -
测试填写了登录字段:
.typeText('#user_login','test_user_testcafe_poc1234@sharklasers.com') -
测试填写了密码字段:
.typeText('#user_password','test_user_testcafe_poc') -
测试填写了确认字段:
.typeText('#user_password_confirmation', 'test_user_testcafe_poc') -
测试填写了名字字段:
.typeText('#user_firstname','test_user') -
测试填写了姓氏字段:
.typeText('#user_lastname','testcafe_poc') -
测试填写了电子邮件字段:
.typeText('#user_mail', 'test_user_testcafe_poc1234@sharklasers.com') -
测试点击了提交按钮:
.click('[value="Submit"]');});注意
您也可以在 GitHub 上查看并下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests3.js。
如您在之前的代码块中所看到的,我们现在使用1234作为随机数。这很简单,但每次我们想要创建一个唯一的用户时,我们都需要手动更新这个数字。让我们通过添加一个简单的开源库来自动完成这项工作,该库将为我们生成时间戳。打开 shell 并执行以下命令:
$ npm install js-automation-tools --save-dev
这将安装js-automation-tools库并将其保存到package.json中我们项目的依赖列表中。现在让我们更新代码以使用这个库生成随机数字:
const { Selector } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits = stamp.getTimestamp();fixture('Redmine log in tests') .page('http://demo.redmine.org/');test('Create a new user', async (t) => { await t.click('.register'); .typeText('#user_login', `test_user_testcafe_ poc${randomDigits}@sharklasers.com`) .typeText('#user_password', 'test_user_testcafe_poc') .typeText('#user_password_confirmation', 'test_user_testcafe_poc') .typeText('#user_firstname', 'test_user') .typeText('#user_lastname', 'testcafe_poc') .typeText('#user_mail', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .click('[value="Submit"]');});
注意
您也可以在 GitHub 上查看并下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests4.js。
如您现在所看到的,每次我们运行测试时,都会生成一个带有唯一数字集的电子邮件,例如test_user_testcafe_poc1588556993141@sharklasers.com,所以我们不再需要担心新用户了。
注意
您可以在此处了解更多关于js-automation-tools库及其所有功能的信息:github.com/Marketionist/js-automation-tools。
因此,既然我们现在有一个创建新 Redmine 用户的测试,让我们继续添加一个登录测试:
const { Selector } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits = stamp.getTimestamp();fixture('Redmine log in tests') .page('http://demo.redmine.org/');test('Create a new user', async (t) => { await t.click('.register'); .typeText('#user_login', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .typeText('#user_password', 'test_user_testcafe_poc') .typeText('#user_password_confirmation', 'test_user_testcafe_poc') .typeText('#user_firstname', 'test_user') .typeText('#user_lastname', 'testcafe_poc') .typeText('#user_mail', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .click('[value="Submit"]');});test('Log in', async (t) => {
测试将执行以下操作:
-
测试点击了登录链接:
await t.click('.login') -
测试填写了登录字段:
.typeText('#username', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) -
测试填写了密码字段:
.typeText('#password', 'test_user_testcafe_poc') -
测试点击了登录按钮:
.click('[name="login"]');});注意
您也可以在 GitHub 上查看并下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests5.js。
由于我们现在已经拥有了创建用户和登录所需的所有操作,让我们添加一些验证来完成这两个测试。
在测试项目中添加验证
通常,每个测试都应该执行一些操作,然后检查结果。正如我们已经在第二章**,探索 TestCafe 内部结构中了解到的那样,TestCafe 为我们提供了t.expect方法来执行断言并验证每个测试的结果。因此,让我们添加相应的断言来完成用户创建和登录测试。
为用户创建测试添加断言
因此,用户创建测试的预期结果是您的账户已激活。您现在可以登录。通知,它应该显示:
const { Selector } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits = stamp.getTimestamp();fixture('Redmine log in tests') .page('http://demo.redmine.org/');test('Create a new user', async (t) => { await t.click('.register'); .typeText('#user_login', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .typeText('#user_password', 'test_user_testcafe_poc') .typeText('#user_password_confirmation', 'test_user_testcafe_poc') .typeText('#user_firstname', 'test_user') .typeText('#user_lastname', 'testcafe_poc') .typeText('#user_mail', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .click('[value="Submit"]') .expect(Selector('#flash_notice').innerText).eql('Your account has been activated. You can now log in.');});
如您在用户创建测试的最终断言中看到的那样,我们通过其 id 获取一个通知元素,然后将其内部文本值与预期结果进行比较。
在登录测试中添加断言
登录测试的预期结果将是一个包含当前活跃用户名的块,它应该显示在页面的右上角:
test('Log in', async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') .expect(Selector('#loggedas').exists).ok();});
备注
您也可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests6.js。
为了展示断言的另一种方法,在登录测试中,我们验证当前活跃用户的用户名块是否在页面上。
添加登出测试
让我们再添加一个登出测试,以完成Redmine 登录测试的固定装置:
const { Selector } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits = stamp.getTimestamp();fixture('Redmine log in tests') .page('http://demo.redmine.org/');// ...test('Log out', async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') .click('.logout') .expect(Selector('#loggedas').exists).notOk() .expect(Selector('.login').exists).ok();});
备注
您也可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests7.js。
如您所见,我们在登出测试中执行了两个断言:
-
验证当前活跃用户的块不在页面上。
-
验证登录链接是否在页面上。
由于我们已经在测试套件中有了三个测试,让我们再添加一些,并查看如何在 TestCafe 中执行自定义代码。
在测试项目中添加自定义代码执行
正如我们已经在第二章**,探索 TestCafe 内部结构中学到的那样,TestCafe 允许您编写在测试页面上执行的代码;这样,您可以获取网页元素、URL 等。特殊类型的函数用于在浏览器客户端执行您的代码:
-
Selector:用于获取任何 DOM 元素。 -
ClientFunction:用于从客户端获取任何数据。
这些函数应与普通异步函数以相同的方式使用,并且您可以使用参数在函数内部传递数据。选择器 API 提供了方法和属性来选择页面上的元素并获取其状态。
为了保持测试的正确结构,建议按测试用例分组。因此,让我们添加一个Redmine entities creation tests测试用例和一个创建新项目测试用例,以查看自定义代码执行的工作方式:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits = stamp.getTimestamp();const getPageUrl = ClientFunction(() => { return window.location.href;});fixture('Redmine log in tests') .page('http://demo.redmine.org/');// ...fixture('Redmine entities creation tests') .page('http://demo.redmine.org/');test('Create a new project', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试点击顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击新项目链接:
.click('.icon-add') -
测试填写名称字段:
.typeText('#project_name', `test_project${randomDigits}`) -
测试点击创建按钮:
.click('[value="Create"]') -
测试验证创建成功通知显示:
.expect(Selector('#flash_notice').innerText).eql('Successful creation.') -
测试验证页面 URL 是否包含项目名称:
.expect(getPageUrl()).contains(`/projects/test_project${randomDigits}/settings`);});注意
您还可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests8.js。
由于我们的第一个测试用例包含了所有登录测试,因此创建了一个新的测试用例来包含所有新实体创建的测试。除此之外,我们还添加了ClientFunction并引入了getPageUrl函数来执行自定义代码并获取当前页面的 URL。
输出将如下所示:

图 4.6 – 带有两个测试用例的 Shell 输出
现在,由于我们有了 Redmine 登录测试和 Redmine 实体创建测试的测试用例,让我们继续填充它们。
添加更多测试
让我们继续编写更多测试,并将它们结构化到按测试用例划分的集合中。
添加新的问题创建测试
我们将从Redmine entities creation tests测试用例中的创建新问题测试用例开始:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');
注意我们正在创建第二组随机数字。我们需要它们,因为现在测试正在创建两个项目,每个项目都应该有一个独特的名称:
const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const getPageUrl = ClientFunction(() => { return window.location.href;});fixture('Redmine log in tests') .page('http://demo.redmine.org/');// ...fixture('Redmine entities creation tests') .page('http://demo.redmine.org/');test('Create a new project', async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') .click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits1}`) .click('[value="Create"]') .expect(Selector('#flash_notice').innerText).eql('Successful creation.') .expect(getPageUrl()).contains(`/projects/test_project${randomDigits1}/settings`);});test('Create a new issue', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试创建一个新的项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_ project${randomDigits2}`).click('[value="Create"]') -
测试点击顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击项目链接:
.click(`[href*="/projects/test_ project${randomDigits2}"]`) -
测试点击新问题链接:
.click('.new-issue') -
测试填写主题字段:
.typeText('#issue_subject', `Test issue ${randomDigits2}`) -
测试填写描述字段:
.typeText('#issue_description', `Test issue description ${randomDigits2}`) -
测试将优先级设置为高:
.click('#issue_priority_id') .click('#issue_priority_id option[value="5"]') -
测试点击创建按钮:
.click('[value="Create"]') -
测试验证创建的通知显示:
.expect(Selector('#flash_notice').innerText).contains('created.');});注意
您还可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests9.js。
添加问题创建显示在项目页面上的测试
让我们继续添加一个测试来验证问题是否显示在项目页面上:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();// ...test('Verify that the issue is displayed on a project page', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试创建一个新项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits3}`) .click('[value="Create"]') -
测试创建一个新问题:
.click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits3}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits3}`) .typeText('#issue_description', `Test issue description ${randomDigits3}`) .click('#issue_priority_id') .click('#issue_priority_id option[value="5"]') .click('[value="Create"]') -
测试点击顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击项目链接:
.click(`[href*="/projects/test_project${randomDigits3}"]`) -
测试点击问题链接:
.click('#main-menu .issues') -
测试验证问题的主题是否显示:
.expect(Selector('.subject a').innerText).contains(`Test issue ${randomDigits3}`);});备注
您也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests10.js。
添加问题编辑测试
现在让我们添加一个新的测试用例,Redmine 实体编辑测试,并添加一个问题编辑测试:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();// ...fixture('Redmine entities editing tests') .page('http://demo.redmine.org/');test('Edit the issue', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_ poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试创建一个新项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits4}`) .click('[value="Create"]') -
测试创建一个新问题:
.click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits4}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits4}`) .typeText('#issue_description', `Test issue description ${randomDigits4}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') -
测试点击顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击项目链接:
.click(`[href*="/projects/test_project${randomDigits4}"]`) -
测试点击问题链接:
.click('#main-menu .issues') -
测试点击问题链接:
.click(Selector('.subject a').withText(`Test issue ${randomDigits4}`)) -
测试点击编辑链接:
.click('.icon-edit') -
测试清除主题字段并填写新的主题:
.selectText('#issue_subject') .pressKey('delete') .typeText('#issue_subject', `Issue ${randomDigits4} updated`) -
测试设置
正常:.click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('Normal')) -
测试点击提交按钮:
.click('[value="Submit"]') -
测试验证成功更新通知是否显示:
.expect(Selector('#flash_notice').innerText).eql('Successful update.');});备注
您也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests11.js。
在此代码示例中有两点值得指出:
-
由于 CSS 选择器无法访问元素的文本,因此使用
.withText方法通过文本获取元素。通过文本定位元素比使用option[value="5"]更稳定,因为如果下拉菜单中添加了更多选项,值属性可能会改变。另一种通过文本获取元素的方法是使用包含文本的 XPath 选择器。 -
使用
.selectText和.pressKey方法清除字段中的当前文本。这种方法模拟了真实用户的行为。选择输入字段中的所有文本并按下删除键盘按钮以删除它。
在项目页面测试中显示添加了更新的问题
现在让我们验证更新的问题是否显示在项目页面上:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();const randomDigits5 = stamp.resetTimestamp();// ...test('Verify that the updated issue is displayed on a project page', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试创建一个新项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits5}`) .click('[value="Create"]') -
测试创建一个新问题:
.click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits5}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits5}`) .typeText('#issue_description', `Test issue description ${randomDigits5}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') -
测试点击顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击项目链接:
.click(`[href*="/projects/test_project${randomDigits5}"]`) -
测试点击问题链接:
.click('#main-menu .issues') -
测试点击问题链接:
.click(Selector('.subject a').withText(`Test issue ${randomDigits5}`)) -
测试点击编辑链接:
.click('.icon-edit') -
测试清除主题字段并填写新的主题:
.selectText('#issue_subject') .pressKey('delete') .typeText('#issue_subject', `Issue ${randomDigits5} updated`) -
测试将优先级设置为正常:
.click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('Normal')) -
测试点击提交按钮:
.click('[value="Submit"]') -
测试点击问题链接:
.click('#main-menu .issues') -
该测试验证更新的问题的主题是否显示:
.expect(Selector('.subject a').innerText).eql(`Issue ${randomDigits5} updated`);});注意
您也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests12.js。
添加问题搜索测试
让我们添加一个用于搜索问题的测试:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();const randomDigits5 = stamp.resetTimestamp();const randomDigits6 = stamp.resetTimestamp();// ...test('Search for the issue', async (t) => {
该测试将执行以下操作:
-
该测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
该测试创建一个新的项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits6}`) .click('[value="Create"]') -
该测试创建一个新的问题:
.click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits6}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits6}`) .typeText('#issue_description', `Test issue description ${randomDigits6}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') -
该测试打开搜索页面:
.navigateTo('http://demo.redmine.org/search') -
该测试将问题的主题输入到搜索字段中:
.typeText('#search-input', `Test issue ${randomDigits6}`) -
该测试点击提交按钮:
.click('[value="Submit"]') -
该测试验证问题是否显示:
.expect(Selector('#search-results').innerText).contains(`Test issue ${randomDigits6}`);});注意
您也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests13.js。
添加问题删除测试
现在我们添加一个Redmine 实体删除测试固定装置和删除问题测试来演示如何处理原生浏览器对话框:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();const randomDigits5 = stamp.resetTimestamp();const randomDigits6 = stamp.resetTimestamp();const randomDigits7 = stamp.resetTimestamp();// ...test('Delete the issue', async (t) => {
该测试将执行以下操作:
-
该测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
该测试创建一个新的项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits7}`) .click('[value="Create"]') -
该测试创建一个新的问题:
.click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits7}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits7}`) .typeText('#issue_description', `Test issue description ${randomDigits7}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') -
该测试点击顶部面板中的项目链接:
.click('#top-menu .projects') -
该测试点击项目链接:
.click(`[href*="/projects/test_project${randomDigits7}"]`) -
该测试点击问题链接:
.click('#main-menu .issues') -
该测试点击问题链接:
.click(Selector('.subject a').withText(`Test issue ${randomDigits7}`)) -
该测试点击删除链接。
-
该测试确认在浏览器模态窗口中删除:
.setNativeDialogHandler(() => true) .click('.icon-del') -
该测试验证问题不会显示:
.expect(Selector('.subject a').withText(`Test issue ${randomDigits7}`).exists).notOk() -
该测试验证是否显示无数据可显示的通知:
.expect(Selector('.nodata').innerText).eql('No data to display');});注意
您也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests14.js。
在这个测试中有一个有趣的事情需要注意:我们在浏览器对话框窗口触发之前使用.setNativeDialogHandler方法。我们在这个方法中传递了一个简单的箭头函数:() => true。它所做的只是返回true。这样做是为了在浏览器对话框窗口出现时回答“确定”(确认)(你可以在第二章**,TestCafe 内部机制和这里:devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/setnativedialoghandler.html了解更多关于此方法的信息)。
添加文件上传测试
为了演示如何处理上传文件,让我们向Redmine 实体创建测试固定装置添加一个上传文件测试。但在那之前,我们需要创建一个用于上传的样本文件,所以打开 shell 并执行以下命令:
$ mkdir -p tests/uploads
$ echo 'test' > uploads/test-file.txt
在前面的命令中,我们在tests文件夹内创建了一个uploads文件夹,然后在其中创建了test-file.txt。
现在,因为我们已经准备了一个虚拟文件(你也可以在 GitHub 上看到:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/tree/master/ch4/test-project/tests/uploads/test-file.txt),让我们创建一个文件上传测试:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();const randomDigits5 = stamp.resetTimestamp();const randomDigits6 = stamp.resetTimestamp();const randomDigits7 = stamp.resetTimestamp();const randomDigits8 = stamp.resetTimestamp();// ...test('Upload a file', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试创建了一个新项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits8}`) .click('[value="Create"]') -
测试点击了顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击了项目链接:
.click(`[href*="/projects/test_project${randomDigits8}"]`) -
测试点击了文件链接:
.click('.files') -
测试点击了新建 文件链接:
.click('.icon-add') -
测试设置文件路径:
.setFilesToUpload('input.file_selector', './uploads/test-file.txt') -
测试点击了添加按钮:
.click('[value="Add"]') -
测试验证文件链接显示:
.expect(Selector('.filename').innerText).eql('test-file.txt') -
测试验证 MD5 校验和显示:
.expect(Selector('.digest').innerText).eql('d8e8fca2dc0f896fd7cb4cb0031ba249');});备注
你也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests15.js。
在这个测试中有一个需要注意的有趣事情:我们使用.setFilesToUpload方法将文件路径注入到页面上的文件上传输入框中(你可以在第二章**,TestCafe 内部机制*,以及这里了解更多关于这个方法的信息:devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/setfilestoupload.html)。
添加文件删除测试
现在让我们向Redmine 实体删除测试固定装置添加一个最后的测试,删除文件:
const { Selector, ClientFunction } = require('testcafe');const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();const randomDigits5 = stamp.resetTimestamp();const randomDigits6 = stamp.resetTimestamp();const randomDigits7 = stamp.resetTimestamp();const randomDigits8 = stamp.resetTimestamp();const randomDigits9 = stamp.resetTimestamp();// ...test('Delete the file', async (t) => {
测试将执行以下操作:
-
测试登录:
await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]') -
测试创建了一个新项目:
.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits9}`) .click('[value="Create"]') -
测试上传了一个新文件:
.click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits9}"]`) .click('.files') .click('.icon-add') .setFilesToUpload('input.file_selector', './uploads/test-file.txt') .click('[value="Add"]') -
测试点击了顶部面板中的项目链接:
.click('#top-menu .projects') -
测试点击了项目链接:
.click(`[href*="/projects/test_project${randomDigits9}"]`) -
测试点击了文件链接:
.click('.files') -
测试点击了垃圾桶图标。
-
测试确认在浏览器模态窗口中删除:
.setNativeDialogHandler(() => true) .click(Selector('.filename a').withText('test-file.txt').parent('.file').find('.buttons a').withAttribute('data-method', 'delete')) -
测试验证文件链接未显示:
.expect(Selector('.filename').withText('test-file.txt').exists).notOk() -
测试验证 MD5 校验和未显示:
.expect(Selector('.digest').withText('d8e8fca2dc0f896fd7cb4cb0031ba249').exists).notOk();});备注
你也可以在 GitHub 上查看和下载此文件:
github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch4/test-project/tests/basic-tests16.js。
在这个测试中需要注意的一个有趣的事情是如何通过一系列方法获取元素:Selector('.filename a').withText('test-file.txt').parent('.file').find('.buttons a').withAttribute('data-method', 'delete')。在这里,我们正在获取一个包含test-file.txt文本的链接,然后搜索其具有file类的父元素,然后在其后代中搜索具有data-method="delete"属性的链接。这将确保我们点击了对应文件的删除链接。您可以在此处了解更多有关如何选择元素的信息:devexpress.github.io/testcafe/documentation/guides/basic-guides/select-page-elements.html。
摘要
在本章中,我们专注于如何为现实生活中的项目编写测试。我们为 Redmine 演示门户制作了四组测试(固定装置):登录测试、实体创建测试、实体编辑测试和实体删除测试。
此外,我们还学习了一些有用的技术,例如如何调试测试、执行自定义代码、断言元素、清除输入、按键、确认原生浏览器警报、上传文件以及链式元素选择器。所有这些课程都可以应用到几乎任何其他 Web 项目中。
由于我们的测试集现在已经准备好了,在下一章中,我们将向当前代码中添加设置和拆卸部分,以改进其结构并增强其可维护性。
第五章:第五章:改进测试
本章的主要学习目标是熟悉如何改进一组端到端测试。这将通过测试设置和清理来实现。此外,我们还将查看不同的命令行设置来运行测试。本章中我们将涵盖的测试技术是通用的,可以重用来为任何 Web 项目编写自动化测试。到本章结束时,我们将拥有一个改进的测试套件,并学习如何使用命令行选项运行它。
本章我们将涵盖以下主要内容:
-
执行选定的测试。
-
探索测试设置和清理。
-
将设置和清理添加到测试项目中。
-
使用命令行设置运行测试。
技术要求
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch5。
执行选定的测试
很常见,在编写或扩展一组测试时,我们需要专注于一个特定的测试,同时忽略所有其他测试。测试通常被组织成集合(测试组也称为固定装置)。幸运的是,TestCafe 提供了fixture.only和test.only方法来指定仅执行选定的测试或固定装置,而其他所有测试将被跳过。让我们以简化形式使用我们的测试集来回顾这一点,其中所有测试操作都被注释掉:
// ...fixture('Redmine log in tests') .page('http://demo.redmine.org/');test.only('Create a new user', async (t) => { /* ... */ });test('Log in', async (t) => { /* ... */ });test('Log out', async (t) => { /* ... */ });fixture('Redmine entities creation tests') .page('http://demo.redmine.org/');test('Create a new project', async (t) => { /* ... */ });test('Create a new issue', async (t) => { /* ... */ });test('Verify that the issue is displayed on a project page', async (t) => { /* ... */ });test('Upload a file', async (t) => { /* ... */ });fixture('Redmine entities editing tests') .page('http://demo.redmine.org/');test('Edit the issue', async (t) => { /* ... */ });test('Verify that the updated issue is displayed on a project page', async (t) => { /* ... */ });test('Search for the issue', async (t) => { /* ... */ });fixture.only('Redmine entities deletion tests') .page('http://demo.redmine.org/');test('Delete the issue', async (t) => { /* ... */ });test('Delete the file', async (t) => { /* ... */ });
如示例所示,test.only在创建新用户测试中使用,而fixture.only在Redmine 实体删除测试固定装置中使用,因此只有创建新用户、删除问题和删除文件测试将被执行。
备注
如果有多个测试(或固定装置)被标记为test.only(或fixture.only),则所有标记的测试和固定装置都将被执行。
此外,TestCafe 允许您使用test.skip和fixture.skip方法来指定在运行测试时跳过的测试或固定装置:
// ...fixture('Redmine log in tests') .page('http://demo.redmine.org/');test('Create a new user', async (t) => { /* ... */ });test.skip('Log in', async (t) => { /* ... */ });test.skip('Log out', async (t) => { /* ... */ });fixture.skip('Redmine entities creation tests') .page('http://demo.redmine.org/');test('Create a new project', async (t) => { /* ... */ });test('Create a new issue', async (t) => { /* ... */ });test('Verify that the issue is displayed on a project page', async (t) => { /* ... */ });test('Upload a file', async (t) => { /* ... */ });fixture('Redmine entities editing tests') .page('http://demo.redmine.org/');test('Edit the issue', async (t) => { /* ... */ });test.skip('Verify that the updated issue is displayed on a project page', async (t) => { /* ... */ });test.skip('Search for the issue', async (t) => { /* ... */ });fixture.skip('Redmine entities deletion tests') .page('http://demo.redmine.org/');test('Delete the issue', async (t) => { /* ... */ });test('Delete the file', async (t) => { /* ... */ });
如前例所示,只有创建新用户和编辑问题测试将被执行。
现在我们已经学会了如何执行特定的测试或固定装置,跳过所有其他测试,让我们看看如何进行测试设置和清理。
探索测试设置和清理
由于测试可能相当长且包含大量重复操作,TestCafe 通过测试设置和清理提供了一种优化方法。
设置通常是在固定装置或测试开始之前执行一系列特定函数(也称为钩子)时进行的(包括fixture.before、fixture.beforeEach和test.before)。
清理通常是在固定装置或测试完成后执行一系列特定函数时进行的(包括fixture.after、fixture.afterEach和test.after)。
在 TestCafe 中有六种使用钩子的方法。
前两个(fixture.before 和 fixture.after)没有访问测试页面,因此应用于执行服务器端操作,例如准备测试应用的服务器或预先在数据库中创建一些测试数据:
-
fixture.before可以用来指定在 fixture 中的第一个测试开始之前应执行的操作 (devexpress.github.io/testcafe/documentation/reference/test-api/fixture/before.html). 在以下示例中,createTestData函数将在My first set of testsfixture 的第一个测试之前被调用:fixture('My first set of tests') .page('https://test-site.com') .before(async (t) => { await createTestData(); }); -
fixture.after可以用来指定在 fixture 中最后一个测试完成后应执行的操作 (devexpress.github.io/testcafe/documentation/reference/test-api/fixture/after.html). 在以下示例中,deleteTestData函数将在My first set of testsfixture 的最后一个测试之后被调用:fixture('My first set of tests') .page('https://test-site.com') .after(async (t) => { await deleteTestData(); });
下面的四个方法(fixture.beforeEach、fixture.afterEach、test.before 和 test.after)在测试的网页已经加载时启动,因此您可以在这些测试钩子内部执行测试操作和其他测试 API 方法:
-
fixture.beforeEach可以用来指定在 fixture 中的每个测试之前应执行的操作 (devexpress.github.io/testcafe/documentation/reference/test-api/fixture/beforeeach.html). 在以下示例中,click操作将在My first set of testsfixture 的每个测试之前执行:fixture('My first set of tests') .page('https://test-site.com') .beforeEach(async (t) => { await t.click('#log-in'); }); -
fixture.afterEach可以用来指定在 fixture 中的每个测试之后应执行的操作 (devexpress.github.io/testcafe/documentation/reference/test-api/fixture/aftereach.html). 在以下示例中,click操作将在My first set of testsfixture 的每个测试之后执行:fixture('My first set of tests') .page('https://test-site.com') .afterEach(async (t) => { await t.click('#delete-test-data'); }); -
test.before可以用来指定在特定测试之前应执行的操作 (devexpress.github.io/testcafe/documentation/reference/test-api/test/before.html). 在以下示例中,click操作将在My first Test测试之前执行:test .before(async (t) => { await t.click('#log-in'); }) ('My first Test', async (t) => { /* ... */ }); -
test.after可以用来指定在特定测试之后应执行的操作 (devexpress.github.io/testcafe/documentation/reference/test-api/test/after.html). 在以下示例中,click操作将在My first Test测试之后执行:test .after(async (t) => { await t.click('#delete-test-data'); }) ('My first Test', async (t) => { /* ... */ });注意
如果一个测试在多个浏览器中运行,测试钩子将在每个浏览器中执行。如果同时使用了
fixture.beforeEach和test.before(或fixture.afterEach和test.after)钩子,则最具体的钩子将覆盖。因此,test.before(或test.after)将被执行,fixture.beforeEach(或fixture.afterEach)将被省略,并且不会为此测试运行。
你可以在devexpress.github.io/testcafe/documentation/guides/basic-guides/organize-tests.html#initialization-and-clean-up了解更多关于钩子的信息。
在本节中,我们介绍了 TestCafe 中可用的钩子类型。现在,让我们将这一知识应用到我们的测试集中。
将设置和清理添加到测试项目
在本节中,我们将看到如何通过设置和清理块来优化我们的测试项目代码。
正如我们在探索测试设置和清理部分中看到的,fixture.beforeEach在需要用户在测试之前登录的每个测试中特别有用。这正是我们的情况,因此让我们将beforeEach块添加到Redmine entities creation tests测试用例中:
// ...fixture('Redmine entities creation tests') .page('http://demo.redmine.org/') .beforeEach(async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]'); });
让我们也从Redmine entities creation tests测试用例的所有测试中移除登录操作,因为这些操作将在beforeEach块中执行。因此,创建一个新项目测试将看起来像这样:
test('Create a new project', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits1}`) .click('[value="Create"]') .expect(Selector('#flash_notice').innerText).eql('Successful creation.') .expect(getPageUrl()).contains(`/projects/test_project${randomDigits1}/settings`);});
在所有登录操作都移动到beforeEach块之后,创建一个新问题测试将看起来像这样:
test('Create a new issue', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits2}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits2}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits2}`) .typeText('#issue_description', `Test issue description ${randomDigits2}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') .expect(Selector('#flash_notice').innerText).contains('created.');});
并且没有登录操作的验证问题是否显示在项目页面上测试将看起来像这样:
test('Verify that the issue is displayed on a project page', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_ project${randomDigits3}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits3}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits3}`) .typeText('#issue_description', `Test issue description ${randomDigits3}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits3}"]`) .click('#main-menu .issues') .expect(Selector('.subject a').innerText).
contains(`Test issue ${randomDigits3}`);});
最后,没有登录操作的上传文件测试将看起来像这样:
test('Upload a file', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits8}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits8}"]`) .click('.files') .click('.icon-add') .setFilesToUpload('input.file_selector', './uploads/test-file.txt') .click('[value="Add"]') .expect(Selector('.filename').innerText).eql('test-file.txt') .expect(Selector('.digest').innerText).eql('d8e8fca2dc0f896fd7cb4cb0031ba249');});
现在,让我们将beforeEach块添加到Redmine entities editing tests测试用例中:
fixture('Redmine entities editing tests') .page('http://demo.redmine.org/') .beforeEach(async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]'); });
让我们也从Redmine entities editing tests测试用例的所有测试中移除登录操作,因为这个操作现在将在beforeEach块中执行。因此,编辑问题测试将看起来像这样:
test('Edit the issue', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits4}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits4}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits4}`) .typeText('#issue_description', `Test issue description ${randomDigits4}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits4}"]`) .click('#main-menu .issues') .click(Selector('.subject a').withText(`Test issue ${randomDigits4}`)) .click('.icon-edit') .selectText('#issue_subject') .pressKey('delete') .typeText('#issue_subject', `Issue ${randomDigits4} updated`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('Normal')) .click('[value="Submit"]') .expect(Selector('#flash_notice').innerText).eql('Successful update.');});
所有登录操作都已移动到beforeEach块,因此验证更新的问题是否显示在项目页面上测试将看起来像这样:
test('Verify that the updated issue is displayed on a project page', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits5}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits5}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits5}`) .typeText('#issue_description', `Test issue description ${randomDigits5}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits5}"]`) .click('#main-menu .issues') .click(Selector('.subject a').withText(`Test issue ${randomDigits5}`)) .click('.icon-edit') .selectText('#issue_subject') .pressKey('delete') .typeText('#issue_subject', `Issue ${randomDigits5} updated`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option'). withText('Normal')) .click('[value="Submit"]') .click('#main-menu .issues') .expect(Selector('.subject a').innerText).eql(`Issue ${randomDigits5} updated`);});
没有所有登录操作的搜索问题测试将看起来像这样:
test('Search for the issue', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_project${randomDigits6}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits6}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits6}`) .typeText('#issue_description', `Test issue description ${randomDigits6}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option'). withText('High')) .click('[value="Create"]') .navigateTo('http://demo.redmine.org/search') .typeText('#search-input', `Test issue ${randomDigits6}`) .click('[value="Submit"]') .expect(Selector('#search-results').innerText).contains(`Test issue ${randomDigits6}`);});
现在,让我们将beforeEach块添加到Redmine entities deletion tests测试用例中:
fixture('Redmine entities deletion tests') .page('http://demo.redmine.org/') .beforeEach(async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]');});
让我们也从Redmine entities deletion tests测试用例的所有测试中移除登录操作,因为这些操作现在将在beforeEach块中执行。因此,删除问题测试将看起来像这样:
test('Delete the issue', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_ project${randomDigits7}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits7}"]`) .click('.new-issue') .typeText('#issue_subject', `Test issue ${randomDigits7}`) .typeText('#issue_description', `Test issue description ${randomDigits7}`) .click('#issue_priority_id') .click(Selector('#issue_priority_id option').withText('High')) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits7}"]`) .click('#main-menu .issues') .click(Selector('.subject a').withText(`Test issue ${randomDigits7}`)) .setNativeDialogHandler(() => true) .click('.icon-del') .expect(Selector('.subject a').withText(`Test issue ${randomDigits7}`).exists).notOk() .expect(Selector('.nodata').innerText).eql('No data to display');});
在所有登录操作都移动到beforeEach块之后,删除文件测试将看起来像这样:
test('Delete the file', async (t) => { await t.click('#top-menu .projects') .click('.icon-add') .typeText('#project_name', `test_ project${randomDigits9}`) .click('[value="Create"]') .click('#top-menu .projects') .click(`[href*="/projects/test_project${randomDigits9}"]`) .click('.files') .click('.icon-add') .setFilesToUpload('input.file_selector', './uploads/test-file.txt') .click('[value="Add"]') .click('#top-menu .projects') .click(`[href*="/projects/test_
project${randomDigits9}"]`) .click('.files') .setNativeDialogHandler(() => true) .click(Selector('.filename a').withText('test-file.txt').parent('.file').find('.buttons a').withAttribute('data-method', 'delete')) .expect(Selector('.filename').withText('test-file.txt').exists).notOk() .expect(Selector('.digest').withText('d8e8fca2dc0f896fd7cb4cb0031ba249').exists). notOk();});
注意
您还可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch5/test-project/tests/basic-tests17.js。
由于我们已经集成了设置和清理块,让我们看看如何使用命令行设置运行测试。
使用命令行设置运行测试
正如我们在第三章,“设置环境”中已经学到的,当你通过执行 testcafe 命令来触发测试时,TestCafe 会从 .testcaferc.json 配置文件中读取设置,如果该文件存在,然后在此基础上应用命令行设置。如果命令行设置与配置文件中的值不同,则命令行设置会覆盖配置文件中的值。TestCafe 会将每个覆盖属性的信息输出到控制台。
注意
如果在配置文件中提供了 browsers 和 src 属性,则可以在命令行中省略它们。
让我们回顾一下在启动测试时可以使用 testcafe 命令的一些主要命令行设置:
-
--help或-h输出所有可用命令行选项的列表(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-h---help)。打开任何 shell 并运行以下命令:$ npx testcafe --help -
--quarantine-mode或-q为失败的测试启用隔离模式(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-q---quarantine-mode)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --quarantine-mode -
--debug-mode或-d逐个执行测试步骤,在每个步骤后暂停测试以进行调试(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-d---debug-mode)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --debug-mode -
--debug-on-fail:如果测试失败,则自动暂停并进入调试模式(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--debug-on-fail)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --debug-on-fail -
--disable-page-caching在测试执行期间禁用浏览器页面缓存(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--disable-page-caching)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --disable-page-caching -
--skip-js-errors, 或-e,确保在测试页面发生 JavaScript 错误时测试不会失败(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-e---skip-js-errors)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --skip-js-errors -
--skip-uncaught-errors,或-u,忽略测试执行期间发生的未捕获错误和未处理的承诺拒绝(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-u---skip-uncaught-errors)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --skip-uncaught-errors -
--test <name>,或-t <name>,仅运行具有指定名称的测试用例(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-t-name---test-name)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --test "Click a link" -
--test-grep <pattern>,或-T <pattern>,仅运行与指定模式匹配的测试用例(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-t-pattern---test-grep-pattern)。例如,要运行名为Click a link、Click a dropdown等测试用例,打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --test-grep "Click.*" -
--fixture <name>,或-f <name>,仅运行具有指定名称的固定测试用例(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-f-name---fixture-name)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --fixture "My first Fixture" -
--fixture-grep <pattern>,或-F <pattern>,仅运行与指定模式匹配的固定测试用例(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-f-pattern---fixture-grep-pattern)。例如,要运行名为Suite1、Suite2等固定测试用例的测试,打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --fixture-grep "Suite.*" -
--test-meta <key=value[,key2=value2,...]>运行其元数据与指定键值对匹配的测试用例(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--test-meta-keyvaluekey2value2)。例如,要运行元数据的suite属性设置为fast且env属性设置为staging的测试用例,打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --test-meta suite=fast,env=staging -
--fixture-meta <key=value[,key2=value2,...]>从符合指定键值对的元数据的测试用例中运行测试 (devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--fixture-meta-keyvaluekey2value2). 例如,要运行设置suite属性为long和env属性为production的元数据的测试用例,打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --fixture-meta suite=long,env=production -
--app <command>或-a <command>在测试开始之前执行指定的 shell 命令,通常用于在运行测试之前使用指定的命令启动测试应用 (devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-a-command---app-command)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --app "node index.js" -
--concurrency <number>或-c <number>通过启动提供的浏览器实例数量并行(同时)运行测试 (devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#-c-n---concurrency-n)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --concurrency 4 -
--speed <factor>设置测试执行的速率,从最慢的0.01到最快的1(devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--speed-factor)。打开任何 shell 并运行以下命令:$ npx testcafe chrome tests/basic-tests.js --speed 0.8
你可以在 devexpress.github.io/testcafe/documentation/reference/command-line-interface.html 上阅读有关所有命令行选项的更多信息。
将所有主要设置保留在 .testcaferc.json 配置文件中是一种良好的实践,在需要时用命令行设置覆盖它们 - 例如,--debug-on-fail --speed 0.8 的组合将非常方便用于调试。
总结来说,在本节中,我们了解了一些主要的命令行设置以及它们在启动测试时的使用方法。
摘要
在本章中,我们探讨了如何选择性地执行测试,以及如何通过测试设置和清理来泛化一些测试操作。此外,我们还回顾了一些用于运行测试的命令行设置。现在我们有一个改进的测试套件,并知道如何使用命令行选项来运行它。
在下一章中,我们将通过将一些测试逻辑移动到单独的函数中,并使用 PageObjects 重构测试来继续改进我们的测试套件。
第六章:第六章:使用 PageObjects 重构
这里的主要学习目标将是熟悉如何使用 TestCafe 角色和 PageObject 模式升级一组端到端测试(测试套件)。我们将使用 Role 来加速测试,并利用 PageObjects 来减少代码重复并提高可维护性。到本章结束时,我们将拥有一组结构化和优化的测试,并了解如何将角色和 PageObject 模式应用于任何未来的项目。
在本章中,我们将涵盖以下主要主题:
-
添加登录
Role。 -
使用
PageObjects重构测试。 -
使用函数改进
PageObjects。
技术要求
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/tree/master/ch6。
添加登录 Role
如我们从 第二章 中所学到的,探索 TestCafe 的内部机制,TestCafe 具有内置的用户角色机制,该机制模拟用户动作以登录网站。角色机制将每个用户的登录状态保存在一个单独的角色中,然后可以在测试的任何部分重复使用该角色以在用户账户之间切换。
因此,让我们首先添加一个 Role 来优化和加速我们在每个测试中执行的登录过程:
const { Selector, ClientFunction, Role } = require('testcafe');// ...const regularUser = Role('http://demo.redmine.org/', async (t) => { await t.click('.login') .typeText('#username', `test_user_testcafe_ poc${randomDigits1}@sharklasers.com`) .typeText('#password', 'test_user_testcafe_poc') .click('[name="login"]');});
如你所见,我们正在使用 Role 内部的登录步骤。这些步骤将在 regularUser 角色首次被调用时执行。一旦登录步骤执行完毕,认证 cookie 和浏览器存储的最终(登录)状态将被保存并用于 regularUser 角色的后续调用(登录步骤将不再执行,因为已保存的登录状态将被应用)。现在,让我们在 Log out 测试中使用 regularUser 角色吧:
test('Log out', async (t) => { await t.useRole(regularUser) .click('.logout') .expect(Selector('#loggedas').exists).notOk() .expect(Selector('.login').exists).ok();});
让我们还在所有其他固定装置的 beforeEach 块中将登录步骤替换为 regularUser 角色吧:
// ...fixture('Redmine entities creation tests') .page('http://demo.redmine.org/') .beforeEach(async (t) => { await t.useRole(regularUser); });// ...fixture('Redmine entities editing tests') .page('http://demo.redmine.org/') .beforeEach(async (t) => { await t.useRole(regularUser); });// ...fixture('Redmine entities deletion tests') .page('http://demo.redmine.org/') .beforeEach(async (t) => { await t.useRole(regularUser); });// ...
注意
你也可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/basic-tests18.js。
利用角色可以加快测试的执行速度,并且我们的测试项目变得更加结构化和细化,这是一个很好的做法。
在我们已经构建了一组测试,通过设置、拆解和角色进行了改进之后,现在让我们通过使用 PageObjects 进行重构,使它们更加有效和易于维护。
使用 PageObjects 重构测试
PageObject 是一种测试自动化模式,它允许你创建一个单独的文件,其中包含选择器和函数,这些选择器和函数代表了被测试页面的抽象。这个单独的文件可以随后被包含并用于测试代码中,以便引用页面元素。
注意,我们的测试中包含过多的代码。例如,CSS 选择器#top-menu .projects(https://github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/basic-tests18.js#L62)在 22 行代码中使用 - 也就是说,每次测试点击PageObjects时,你都可以将所有选择器放在一个地方,这样下次网页发生变化时,你只需修改PageObject文件即可。
因此,让我们在tests文件夹内创建一个简单的redmine-page.js文件。打开任何 shell,转到test-project文件夹,并执行以下命令:
$ touch tests/redmine-page.js
现在,在您选择的代码编辑器中打开redmine-page.js文件,在redminePage对象内添加声明linkProjects选择器的代码,然后导出此对象:
let redminePage = { linkProjects: '#top-menu .projects'};module.exports = redminePage;
注意
您也可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/redmine-page1.js。
因此,我们创建了一个包含linkProjects属性的对象,该属性包含此元素的选择器。现在我们需要将此对象包含在我们的测试中:
const { Selector, ClientFunction, Role } = require('testcafe');const { stamp } = require('js-automation-tools');const redminePage = require('./redmine-page.js');// ...
此外,我们还需要将所有#top-menu .projects的出现替换为相应的PageObject元素。因此,更新后的创建新项目测试将如下所示:
test('Create a new project', async (t) => { await t.click(redminePage.linkProjects) .click('.icon-add') .typeText('#project_name', `test_project${randomDigits1}`) .click('[value="Create"]') .expect(Selector('#flash_notice').innerText).eql('Successful creation.') .expect(getPageUrl()).contains(`/projects/test_project${randomDigits1}/settings`);});
注意
您也可以在 GitHub 上查看和下载此文件,其中所有测试都已更新以使用redminePage.linkProjects:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/basic-tests19.js。
现在,将所有带有随机数字生成的常量移动到redmine-page.js中,因为所有使用随机数字的字符串也将移动到redmine-page.js中:
const { stamp } = require('js-automation-tools');const randomDigits1 = stamp.getTimestamp();const randomDigits2 = stamp.resetTimestamp();const randomDigits3 = stamp.resetTimestamp();const randomDigits4 = stamp.resetTimestamp();const randomDigits5 = stamp.resetTimestamp();const randomDigits6 = stamp.resetTimestamp();const randomDigits7 = stamp.resetTimestamp();const randomDigits8 = stamp.resetTimestamp();const randomDigits9 = stamp.resetTimestamp();
现在,让我们将所有选择器移动到redmine-page.js文件中的redminePage对象内。我们将从登录凭据开始:
let redminePage = { urlRedmine: 'http://demo.redmine.org/', emailRegularUser: `test_user_testcafe_poc${randomDigits1}@sharklasers.com`, passwordRegularUser: 'test_user_testcafe_poc',
现在,让我们添加页面元素的选择器:
linkLogin: '.login', inputUsername: '#username', inputPassword: '#password', buttonLogin: '[name="login"]', linkRegister: '.register', inputUserLogin: '#user_login', inputUserPassword: '#user_password', inputUserPasswordConfirmation: '#user_password_confirmation', inputUserFirstName: '#user_firstname', inputUserLastName: '#user_lastname', inputUserMail: '#user_mail', buttonSubmit: '[value="Submit"]', blockNotification: '#flash_notice', blockLoggedAs: '#loggedas', linkLogout: '.logout', linkProjects: '#top-menu .projects', iconAdd: '.icon-add', inputProjectName: '#project_name', buttonCreate: '[value="Create"]', urlProjectSettings: `/projects/test_project${randomDigits1}/settings`, link2TestProject: `[href*="/projects/test_project${randomDigits2}"]`,// ...
最后,让我们增强redminePage对象,添加包含文本的属性:
textFirstNameRegularUser: 'test_user', textLastNameRegularUser: 'testcafe_poc', textAccountActivated: 'Your account has been activated. You can now log in.', text1ProjectName: `test_project${randomDigits1}`, text2ProjectName: `test_project${randomDigits2}`,// ...};module.exports = redminePage;
注意
您也可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/redmine-page2.js。
如您所见,URL 如http://demo.redmine.org/和通知文本字符串如'您的账户已激活。您现在可以登录。'也可以通过PageObjects轻松重构。
现在,让我们在每个测试中使用PageObject元素。因此,更新的Redmine 登录测试固定装置将如下所示:
const { Selector, ClientFunction, Role } = require('testcafe');const redminePage = require('./redmine-page.js');const getPageUrl = ClientFunction(() => { return window.location.href;});const regularUser = Role(redminePage.urlRedmine, async (t) => { await t.click(redminePage.linkLogin) .typeText(redminePage.inputUsername, redminePage.emailRegularUser) .typeText(redminePage.inputPassword, redminePage.passwordRegularUser) .click(redminePage.buttonLogin);});fixture('Redmine log in tests').page(redminePage.urlRedmine);
带有PageObject元素的创建新用户测试将如下所示:
test('Create a new user', async (t) => { await t.click(redminePage.linkRegister) .typeText(redminePage.inputUserLogin, redminePage.emailRegularUser) .typeText(redminePage.inputUserPassword, redminePage.passwordRegularUser) .typeText(redminePage.inputUserPasswordConfirmation, redminePage.passwordRegularUser) .typeText(redminePage.inputUserFirstName, redminePage.textFirstNameRegularUser) .typeText(redminePage.inputUserLastName, redminePage.textLastNameRegularUser) .typeText(redminePage.inputUserMail, redminePage.emailRegularUser) .click(redminePage.buttonSubmit) .expect(Selector(redminePage.blockNotification).innerText).eql(redminePage.textAccountActivated);});
登录和登出测试将如下所示:
test('Log in', async (t) => { await t.click(redminePage.linkLogin) .typeText(redminePage.inputUsername, redminePage.emailRegularUser) .typeText(redminePage.inputPassword, redminePage.passwordRegularUser) .click(redminePage.buttonLogin) .expect(Selector(redminePage.blockLoggedAs).exists).ok();});test('Log out', async (t) => { await t.useRole(regularUser) .click(redminePage.linkLogout) .expect(Selector(redminePage.blockLoggedAs).exists).notOk() .expect(Selector(redminePage.linkLogin).exists).ok();});
更新后的Redmine 实体创建测试固定装置将看起来如下:
fixture('Redmine entities creation tests') .page(redminePage.urlRedmine) .beforeEach(async (t) => { await t.useRole(regularUser); });
创建新项目和创建新问题测试将如下所示:
test('Create a new project', async (t) => { await t.click(redminePage.linkProjects) .click(redminePage.iconAdd) .typeText(redminePage.inputProjectName, redminePage.text1ProjectName) .click(redminePage.buttonCreate) .expect(Selector(redminePage.blockNotification).innerText).eql(redminePage.textSuccessfulCreation) .expect(getPageUrl()).contains(redminePage. urlProjectSettings);});test('Create a new issue', async (t) => { await t.click(redminePage.linkProjects) .click(redminePage.iconAdd) .typeText(redminePage.inputProjectName, redminePage.text2ProjectName) .click(redminePage.buttonCreate) .click(redminePage.linkProjects) .click(redminePage.link2TestProject) .click(redminePage.linkNewIssue) .typeText(redminePage.inputIssueSubject, redminePage.text2IssueName) .typeText(redminePage.inputIssueDescription, redminePage.text2IssueDescription) .click(redminePage.dropdownIssuePriority) .click(Selector(redminePage.optionIssuePriority).withText(redminePage.textHigh)) .click(redminePage.buttonCreate) .expect(Selector(redminePage.blockNotification).innerText).contains(redminePage.textCreated);});
注意
您还可以在 GitHub 上审查和下载所有测试更新为使用PageObjects的文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/basic-tests20.js。
在本节中,我们学习了如何使用PageObjects增强测试,并相应地重构了我们的测试项目。现在,让我们通过向PageObject添加函数来进一步提高测试的可维护性。
使用函数改进 PageObjects
如我们在redmine-page.js中观察到的,PageObject内部的一些属性仍然包含一些重复的代码。让我们通过将此类重复代码移动到单独的函数中来进一步优化我们的PageObject:
// ...const createButtonSelector = (text) => { return `[value="${text}"]`;};const createLinkTestProjectSelector = (randomDigits) => { return `[href*="/projects/test_project${randomDigits}"]`;};const createProjectNameText = (randomDigits) => { return `test_project${randomDigits}`;};const createIssueNameText = (randomDigits) => { return `Test issue ${randomDigits}`;};const createIssueDescriptionText = (randomDigits) => { return `Test issue description ${randomDigits}`;};const createIssueNameUpdatedText = (randomDigits) => { return `Issue ${randomDigits} updated`;};redminePage.buttonLogin = createButtonSelector('Login »');redminePage.buttonSubmit = createButtonSelector('Submit');redminePage.buttonCreate = createButtonSelector('Create');redminePage.buttonAdd = createButtonSelector('Add');
注意,buttonLogin选择器已更新。之前它是[name="login"],但现在createButtonSelector函数将返回它为[value="Login »"]。这样做是为了通用我们的选择器生成,因此现在buttonLogin选择器是用与所有其他按钮元素相同的createButtonSelector函数创建的。
让我们现在生成一组测试项目链接的选择器:
redminePage.link2TestProject = createLinkTestProjectSelector(randomDigits2);redminePage.link3TestProject = createLinkTestProjectSelector(randomDigits3);redminePage.link4TestProject = createLinkTestProjectSelector(randomDigits8);redminePage.link5TestProject = createLinkTestProjectSelector(randomDigits4);redminePage.link6TestProject = createLinkTestProjectSelector(randomDigits5);redminePage.link7TestProject = createLinkTestProjectSelector(randomDigits6);redminePage.link8TestProject = createLinkTestProjectSelector(randomDigits7);redminePage.link9TestProject = createLinkTestProjectSelector(randomDigits9);
现在,让我们生成一组测试项目名称的文本:
redminePage.text1ProjectName = createProjectNameText(randomDigits1);redminePage.text2ProjectName = createProjectNameText(randomDigits2);redminePage.text3ProjectName = createProjectNameText(randomDigits3);redminePage.text4ProjectName = createProjectNameText(randomDigits8);redminePage.text5ProjectName = createProjectNameText(randomDigits4);redminePage.text6ProjectName = createProjectNameText(randomDigits5);redminePage.text7ProjectName = createProjectNameText(randomDigits6);redminePage.text8ProjectName = createProjectNameText(randomDigits7);redminePage.text9ProjectName = createProjectNameText(randomDigits9);
最后,让我们生成一组包含问题名称(例如,Test issue 1598717241841)和问题描述(例如,Test issue description 1598717241841)文本的对象属性:
redminePage.text2IssueName = createIssueNameText(randomDigits2);redminePage.text3IssueName = createIssueNameText(randomDigits3);redminePage.text5IssueName = createIssueNameText(randomDigits4);redminePage.text6IssueName = createIssueNameText(randomDigits5);redminePage.text7IssueName = createIssueNameText(randomDigits6);redminePage.text8IssueName = createIssueNameText(randomDigits7);redminePage.text2IssueDescription = createIssueDescriptionText(randomDigits2);redminePage.text3IssueDescription = createIssueDescriptionText(randomDigits3);redminePage.text5IssueDescription = createIssueDescriptionText(randomDigits4);redminePage.text6IssueDescription = createIssueDescriptionText(randomDigits5);redminePage.text7IssueDescription = createIssueDescriptionText(randomDigits6);redminePage.text8IssueDescription = createIssueDescriptionText(randomDigits7);redminePage.text5IssueNameUpdated = createIssueNameUpdatedText(randomDigits4);redminePage.text6IssueNameUpdated = createIssueNameUpdatedText(randomDigits5);module.exports = redminePage;
注意
您还可以使用PageObject函数审查和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/redmine-page3.js,以及相应的测试文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch6/test-project/tests/basic-tests21.js。
因此,现在我们有函数来创建类似选择器和文本的组。这种技术最终非常有用,因为可以通过更改创建它们的相应函数来在一个地方编辑一组类似元素。
摘要
在本章中,我们探讨了如何在登录时使用Role来加速测试执行,使用PageObject重构了测试,并通过函数改进了PageObject。现在我们有一组快速且易于维护和扩展的测试(如果将来有必要的话)。这些知识可以用来重构现有测试,或者构建一组新的健壮且易于维护的自动化测试集。
在下一章中,我们将总结测试项目,并快速浏览 TestCafe 的未来。
第七章:第七章: TestCafe 的发现
本章的主要学习目标是使用函数优化我们的测试动作,并熟悉如何使用 npm 脚本来运行测试。我们还将回顾 TestCafe 框架发展的主要方向,以及一些有用资源的引用。
这将给我们一些额外的想法,关于如何重构测试,如何更有效地运行它们,以及在哪里寻找进一步的改进。
在本章中,我们将涵盖以下主要内容:
-
使用测试函数迈出最后一步。
-
使用 npm 脚本来封装测试项目。
-
探索 TestCafe 的开发和未来计划。
-
对有用资源的额外引用。
技术要求
本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch7。
使用测试函数迈出最后一步
我们创建的测试由一系列动作组成。其中一些,例如Creating a new project测试,仍在重复进行。所以,最后一步逻辑步骤将是将这些动作序列分离到函数中。让我们看看如何通过redmine-page.js中的createNewProject、createNewIssue和uploadFile函数来实现这一点:
const { Selector, ClientFunction, Role, t } = require('testcafe');const { stamp } = require('js-automation-tools');// ...redminePage.getPageUrl = ClientFunction(() => { return window.location.href;});redminePage.regularUser = Role(redminePage.urlRedmine, async (t) => { await t.click(redminePage.linkLogin) .typeText(redminePage.inputUsername, redminePage.emailRegularUser) .typeText(redminePage.inputPassword, redminePage. passwordRegularUser) .click(redminePage.buttonLogin);});
如您所见,我们将getPageUrl和regularUser移动到了redmine-page.js,因为将所有实用函数集中在一个文件中非常方便。
现在,让我们添加createNewProject函数,该函数将包含创建新项目的所有动作:
redminePage.createNewProject = async (textProjectName) => { await t.click(redminePage.linkProjects) .click(redminePage.iconAdd) .typeText(redminePage.inputProjectName, textProjectName) .click(redminePage.buttonCreate);};
需要添加另一个函数,该函数将包含创建新问题的所有动作:
redminePage.createNewIssue = async ( linkTestProject, textIssueName, textIssueDescription ) => { await t.click(redminePage.linkProjects) .click(linkTestProject) .click(redminePage.linkNewIssue) .typeText(redminePage.inputIssueSubject, textIssueName) .typeText(redminePage.inputIssueDescription, textIssueDescription) .click(redminePage.dropdownIssuePriority) .click(Selector(redminePage.optionIssuePriority).withText(redminePage.textHigh)) .click(redminePage.buttonCreate);};
最后,添加包含上传文件所有动作的函数:
redminePage.uploadFile = async (linkTestProject) => { await t.click(redminePage.linkProjects) .click(linkTestProject) .click(redminePage.linkFiles) .click(redminePage.iconAdd) .setFilesToUpload(redminePage.inputChooseFiles, redminePage.pathToFile) .click(redminePage.buttonAdd);};module.exports = redminePage;
注意
您也可以在 GitHub 上查看和下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch7/test-project/tests/redmine-page4.js。
现在,更新的Create a new project测试将看起来像这样:
const { Selector } = require('testcafe');const redminePage = require('./redmine-page.js');// ...fixture('Redmine entities creation tests') .page(redminePage.urlRedmine) .beforeEach(async (t) => { await t.useRole(redminePage.regularUser); });test('Create a new project', async (t) => { await redminePage.createNewProject(redminePage.text1ProjectName); await t.expect(Selector(redminePage.blockNotification).innerText).eql(redminePage.textSuccessfulCreation) .expect(redminePage.getPageUrl()).contains(redminePage.urlProjectSettings);});
如您可能已注意到,现在,从testcafe中只需要Selector,因为ClientFunction、Role和t已被移动到redmine-page.js。此外,我们现在使用redminePage.regularUser而不是仅仅regularUser - 这是因为将regularUser函数移动到了redmine-page.js。
更新的Create a new issue测试将看起来像这样:
test('Create a new issue', async (t) => { await redminePage.createNewProject(redminePage.text2ProjectName); await redminePage.createNewIssue( redminePage.link2TestProject, redminePage.text2IssueName, redminePage.text2IssueDescription ); await t.expect(Selector(redminePage.blockNotification).innerText).contains(redminePage.textCreated);});
现在的Verify that the issue is displayed on a project page测试看起来也会更短,因为我们现在在测试中使用createNewProject和createNewIssue函数来创建相应的实体:
test('Verify that the issue is displayed on a project page', async (t) => { await redminePage.createNewProject(redminePage.text3ProjectName); await redminePage.createNewIssue( redminePage.link3TestProject, redminePage.text3IssueName, redminePage.text3IssueDescription ); await t.click(redminePage.linkProjects) .click(redminePage.link3TestProject) .click(redminePage.linkIssues) .expect(Selector(redminePage.linkIssueName).innerText).contains(redminePage.text3IssueName);});
在Upload a file测试中,我们将利用createNewProject和uploadFile函数,使其看起来也更紧凑:
test('Upload a file', async (t) => { await redminePage.createNewProject(redminePage.text4ProjectName); await redminePage.uploadFile(redminePage.link4TestProject); await t.expect(Selector(redminePage.linkFileName).innerText).eql(redminePage.textFileName) .expect(Selector(redminePage.blockDigest).innerText).eql(redminePage.textChecksum);});
下面是使用 createNewProject 和 createNewIssue 函数更新的 Edit the issue 测试的显示方式:
// ...test('Edit the issue', async (t) => { await redminePage.createNewProject(redminePage.text5ProjectName); await redminePage.createNewIssue( redminePage.link5TestProject, redminePage.text5IssueName, redminePage.text5IssueDescription ); await t.click(redminePage.linkProjects) .click(redminePage.link5TestProject) .click(redminePage.linkIssues) .click(Selector(redminePage.linkIssueName).withText(redminePage.text5IssueName)) .click(redminePage.iconEdit) .selectText(redminePage.inputIssueSubject) .pressKey(redminePage.keyDelete) .typeText(redminePage.inputIssueSubject, redminePage.text5IssueNameUpdated) .click(redminePage.dropdownIssuePriority) .click(Selector(redminePage.optionIssuePriority).withText(redminePage.textNormal)) .click(redminePage.buttonSubmit) .expect(Selector(redminePage.blockNotification).innerText).eql(redminePage.textSuccessfulUpdate);});
并且使用 createNewProject 和 createNewIssue 函数重构的 Verify that the updated issue is displayed on a project page 测试现在看起来是这样的:
test('Verify that the updated issue is displayed on a project page', async (t) => { await redminePage.createNewProject(redminePage.text6ProjectName); await redminePage.createNewIssue( redminePage.link6TestProject, redminePage.text6IssueName, redminePage.text6IssueDescription ); await t.click(redminePage.linkProjects) .click(redminePage.link6TestProject) .click(redminePage.linkIssues) .click(Selector(redminePage.linkIssueName). withText(redminePage.text6IssueName)) .click(redminePage.iconEdit) .selectText(redminePage.inputIssueSubject) .pressKey(redminePage.keyDelete) .typeText(redminePage.inputIssueSubject, redminePage.text6IssueNameUpdated) .click(redminePage.dropdownIssuePriority) .click(Selector(redminePage.optionIssuePriority).withText(redminePage.textNormal)) .click(redminePage.buttonSubmit) .click(redminePage.linkIssues) .expect(Selector(redminePage.linkIssueName).innerText).eql(redminePage.text6IssueNameUpdated);});
Search for the issue 测试也将从利用 createNewProject 和 createNewIssue 函数中受益,因为它将显著缩短:
test('Search for the issue', async (t) => { await redminePage.createNewProject(redminePage.text7ProjectName); await redminePage.createNewIssue( redminePage.link7TestProject, redminePage.text7IssueName, redminePage.text7IssueDescription ); await t.navigateTo(redminePage.urlRedmineSearch) .typeText(redminePage.inputSearch, redminePage.text7IssueName) .click(redminePage.buttonSubmit) .expect(Selector(redminePage.blockSearchResults).innerText).contains(redminePage.text7IssueName);});
最后,以下是重构的 Delete the issue 和 Delete the file 测试的显示方式:
// ...test('Delete the issue', async (t) => { await redminePage.createNewProject(redminePage.text8ProjectName); await redminePage.createNewIssue( redminePage.link8TestProject, redminePage.text8IssueName, redminePage.text8IssueDescription ); await t.click(redminePage.linkProjects) .click(redminePage.link8TestProject) .click(redminePage.linkIssues) .click(Selector(redminePage.linkIssueName).withText(redminePage.text8IssueName)) .setNativeDialogHandler(() => true) .click(redminePage.iconDelete) .expect(Selector(redminePage.linkIssueName).withText(redminePage.text8IssueName).exists).notOk() .expect(Selector(redminePage.blockNoData).innerText).eql(redminePage.textNoData);});test('Delete the file', async (t) => { await redminePage.createNewProject(redminePage.text9ProjectName); await redminePage.uploadFile(redminePage.link9TestProject); await t.click(redminePage.linkProjects) .click(redminePage.link9TestProject) .click(redminePage.linkFiles) .setNativeDialogHandler(() => true) .click(Selector(redminePage.linkFileName).withText(redminePage.textFileName).parent(redminePage.blockFile).find(redminePage.buttonAction).withAttribute('data-method', redminePage.dataMethodDelete)) .expect(Selector(redminePage.linkFileName).withText(redminePage.textFileName).exists).notOk() .expect(Selector(redminePage.blockDigest).withText(redminePage.textChecksum).exists).notOk();});
注意
您也可以在 GitHub 上查看并下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch7/test-project/tests/basic-tests22.js。
因此,我们已经优化了我们的测试集,使其更细粒度并使用函数而不是重复操作。现在,让我们使用 npm 脚本来完成测试项目。
使用 npm 脚本完成测试项目
由于我们已经完成了测试的重构,让我们看看如何更有效地运行它们。如我们回忆在 第三章**,设置环境,我们初始化了 package.json,以及 第四章**,使用 TestCafe 构建测试套件,我们添加了 js-automation-tools 库,我们的基本 package.json 文件目前看起来是这样的:
{ "name": "test-project", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "js-automation-tools": "¹.0.5", "testcafe": "¹.8.7" }}
我们目前通过执行以下命令来运行我们的测试:
$ npx testcafe chrome tests/basic-tests.js
或者,如我们讨论的 第五章,改进测试,我们利用双横线调试失败标志来使我们的开发生活更轻松(这将在测试失败时暂停测试,并允许您查看测试页面并确定失败的原因):
$ npx testcafe chrome tests/basic-tests.js --debug-on-fail
我们还可以使用一个额外的标志:--speed(设置测试执行速率)——以降低测试执行速度进行调试:
$ npx testcafe chrome tests/basic-tests.js --debug-on-fail --speed 0.8
现在看起来相当长了,不是吗?为了克服这个问题,我们可以使用 npm 脚本。让我们在 package.json 中创建一个 test-debug 别名来带有调试标志启动测试:
{ "name": "test-project", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test-debug": "testcafe chrome tests/basic-tests.js --debug-on-fail --speed 0.8" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "js-automation-tools": "¹.0.5", "testcafe": "¹.8.7" }}
因此,现在我们可以通过执行我们刚刚创建的别名的一个简短命令来带有调试标志运行我们的测试:
$ npm run test-debug
正如我们讨论了如何使用 npm 脚本来添加本地测试调试的命令,现在让我们想象我们还需要在 --quarantine-mode 标志下运行我们的测试:
{ "name": "test-project", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test-ci": "testcafe chrome tests/basic-tests.js --quarantine-mode", "test-debug": "testcafe chrome tests/basic-tests.js --debug-on-fail --speed 0.8" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "js-automation-tools": "¹.0.5", "testcafe": "¹.8.7" }}
注意
您也可以在 GitHub 上查看并下载此文件:github.com/PacktPublishing/Modern-Web-Testing-with-TestCafe/blob/master/ch7/test-project/package.json。
因此,现在我们可以通过执行一个简短且简单的命令在 CI 上运行我们的测试:
$ npm run test-ci
总结来说,在本节中,我们讨论了如何使用 npm 脚本来创建本地和远程(持续集成)命令的简短别名。
现在,让我们探讨如何保持对 TestCafe 开发的关注,以及在哪里寻找进一步的改进。
探索 TestCafe 的开发和未来计划
TestCafe 的诞生可以追溯到 2010 年初,当时来自 DevExpress 的开发者开始着手开发它。最初,当它在 2013 年发布时,它是一个商业测试框架。到了 2016 年,决定开源 TestCafe 的核心。从那时起,每月下载量已超过 76 万次,并且这个数字仍在增长。DevExpress 还发布了一个名为 TestCafe Studio 的商业测试 IDE(www.devexpress.com/products/testcafestudio/),它是基于开源的 TestCafe 核心构建的。因此,看起来 TestCafe 将会持续存在。DevExpress 将继续开发它,因为这将为 TestCafe Studio 添加新功能。
让我们回顾一下 TestCafe 的一些优点:
-
开源。
-
安装简单快捷。
-
无头测试。
-
开箱即用的跨平台和跨浏览器支持。
-
支持最受欢迎的 Web 开发编程语言之一:JavaScript/TypeScript。
-
清晰、灵活且文档齐全的 API。
-
开箱即用的智能断言和自动等待机制。
-
为浏览器提供商、框架特定选择器、自定义报告器、Cucumber 支持等提供免费自定义插件。
关于 TestCafe 未来开发方向,根据路线图(devexpress.github.io/testcafe/roadmap/),有一个计划通过添加发送 HTTP 请求和检查响应细节的方法来支持 API 测试。除此之外,TestCafe 团队正在积极开发多浏览器窗口功能,并计划进一步改进 TestCafe 的调试流程。
另一条值得提到的建议:关注 TestCafe 的变更日志(github.com/DevExpress/testcafe/blob/master/CHANGELOG.md)。它包含了大量关于新功能和更新的有用信息。这样,你将始终知道何时发布新版本,以及可以期待什么。
在我们回顾了 TestCafe 的开发并触及了其未来计划之后,现在让我们探索一些可用于进一步使用 TestCafe 进行测试自动化的资源。
其他有用的资源参考
这里有一些关于 TestCafe 的优秀信息来源:
-
TestCafe 文档:
devexpress.github.io/testcafe/documentation/reference/。 -
TestCafe 变更日志:
github.com/DevExpress/testcafe/blob/master/CHANGELOG.md。 -
TestCafe 未来路线图:
devexpress.github.io/testcafe/roadmap/和github.com/DevExpress/testcafe/projects. -
TestCafe 团队博客:
devexpress.github.io/testcafe/media/team-blog/. -
Stack Overflow 过滤器,用于最近关于 TestCafe 的问题:
stackoverflow.com/questions/tagged/testcafe.
摘要
在本章中,我们探讨了如何使用函数优化测试操作,以及如何使用 npm 脚本来运行测试。我们还回顾了 TestCafe 框架的发展,以及一些有用的资源参考。这些技能和经验旨在通过强调如何重构测试、如何更有效地运行它们以及如何寻找进一步的改进,来帮助你进行任何进一步的测试自动化开发。
这标志着我们对 TestCafe,测试自动化领域的新星,富有成效的探索的结束。我希望你喜欢这次探索,并在未来的项目中继续使用这个出色的工具!


浙公网安备 33010602011771号