我们来看一则小故事:
一个初创公司,在早期,所有的业务界面都在一个前端工程项目中,工程里有五个界面,一个研发加上一个测试,就能保证每次发布的新功能稳定可靠。
随着公司业务蓬勃发展,当年的初创公司慢慢变得越来越壮大,业务界面变得越来越多,原来的那个前端工程项目从只有五个界面,增长到现在的几十个界面。
并且,业务线也变多了。通过对业务线的划分,甚至同一个业务线内,不同类型的模块划分,公司的业务界面总量达到上千个,分别存放在几十个前端工程项目中。
相对的,前端团队的研发人员和测试人员的数量也由原来的两个人,增长到现在的几十人。
这时,团队遭遇了一个问题:每次发布上线的功能,变得不再稳定可靠,经常出现各种各样的问题。
这个状态一直在持续,而公司也不得不持续不断得为这些问题造成的影响买单。
公司开始着手调查这个问题的本质原因。公司派出了两位调查员:小A 和 小B。
小A认为人才资源还是不足,需要招聘更多的人,让每个前端业务项目都能有足够的人来维护,借鉴早期的团队结构,也就是达到每个工程五个界面,一个研发和一个测试的人员规模。
小B认为现有前端团队的工作方式需要调整,以适应大规模的前端工程维护与建设。并给出了两个实际案例:
案例一:某个前端工程内有一百多个界面,最近用户反馈某个界面打开后没有内容。反馈信息到达研发团队,研发人员调查发现在几个月之前这个界面被其它界面的修改影响了,导致此问题。当时测试人员并没有发现这个问题,因为一方面测试人员并没有这个模块所有界面的名单;另一方面当时修改的是另外的界面,测试人员将精力放在那些被修改的界面上了。最后,研发人员修复了这个界面,并交给测试人员进行测试,测试人员测试通过后发布上线,修复了这个问题。
案例二:某个前端工程最近没有发布动作,但某天突然收到用户反馈:创建订单时,提示订单创建失败。反馈信息到达研发团队,研发人员调查发现最近安全策略模块上线更新了某条安全策略,新的安全策略规则过于宽泛,阻止了这位反馈用户的订单内容。一方面研发人员引导用户修复了非法的订单内容;另一方面,研发人员调整了导致问题的安全策略规则,最终解决了这个问题。
在参考了两位调查员的报告后,团队做出了如下调整:
在测试环境增加自动测试,自动测试分为两个类型:冒烟测试(探测界面是否有内容) 和 功能测试(探测功能是否畅通)
自动测试的时间设定为:上午10点 和 晚上6点
测试的结果以交通灯的方式展示,并挂钩到发布平台:红色灯代表测试失败、黄色灯代表测试未完成、绿色灯代表测试通过
发布平台只允许绿色灯的工程项目发布上线。
这则小故事结束了,我们来回顾这则故事中出现的两个问题:
- 随着工程模块内的复杂度不断增加,靠人的精力来覆盖工程内的方方面面,已不切实际。
- 随着不同工程模块的规模不断壮大,靠人的精力来控制不同工程之间的关系和影响,已不切实际。
专业的工程师都明白一个道理:人是很容易犯错的,一个功能,靠人自觉或小心谨慎地来维持,无异于作茧自缚。
测试
是一个很大的话题,我们今天来聊一下 测试驱动开发
。
说起 测试驱动开发
(Test Driven Development, TDD),相信很多读者都并不陌生。
测试驱动开发(英语:Test-driven development,缩写为TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发始于20世纪90年代。测试驱动开发的目的是取得快速反馈并使用“illustrate the main line”方法来构建程序。
测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。
--- 维基百科
要说在实际工作中使用 TDD 的方式进行开发,可能只有少数人会这么做。
因为,考虑到有限的、甚至急迫的业务研发周期,相信很多读者都会感到担忧:测试会占用开发周期,不如直接开发功能更 “省” 时间。
如果我们用更长远的视角来看研发这件事情,就会发现,实际上对于直接开发功能的方式,只是将软件工程的时间提前消费了,后期还是得将前期 “省” 去的那部分时间还回来,甚至还要支付昂贵的利息(当然了,重视短期利益的人并不会想那么远)。
测试的重要性不言而喻,我们发现,将测试放在软件工程的越靠前的环节中,它就越能帮助到工程本身,因为:
- 测试能在代码投入生产之前,发现其中潜在的问题,避免损失
- 测试能促使代码变得更加健壮,提高可维护性,从而降低成本
- 测试能发现设计的不良之处,促进改良设计,从而提高服务质量
今天,我们来学习一个叫做 Cypress
的测试工具,使用这个工具,来帮助我们进行前端功能研发。
我们按照下面的步骤逐步讲解:
- 什么是
Cypress
- 如何安装
Cypress
- 开发前:如何写基于
Cypress
的测试文件 - 开发中:如何使用
Cypress
测试文件辅助我们进行测试 - 如何更方便得写
Cypress
测试文件
1. 什么是 Cypress
cypress 是一个完整的,易用的测试框架
我们可以使用 Cypress
进行: e2e
测试、集成测试、单元测试
Cypress 官网介绍了以下功能特点:
- 时间旅行:
Cypress
会在测试运行是拍摄快照,只需将鼠标悬停在命令日志中的命令上,即可确切了解每个步骤发生的情况。 - 可调式:我们无需猜测测试用例为何失败,直接从熟悉的工具进行调试(例如:谷歌浏览器的开发者工具),可读错误和堆栈跟踪让调试更有效率。
- 自动等待:再也无需在测试用例代码中添加
wait
或sleep
代码,Cypress
会自动等待命令和断言完成。 - 函数间谍、响应劫持、时钟回拨:验证和控制函数、服务器响应和时钟。常用的单元测试功能,cypress 都已提供于指尖。
- 网络通信控制:无需涉及服务器即可控制、保存和测试边缘情况。你可以根据需要保留网络流量。
- 一致的结果:
Cypress
的架构不使用Selenium
或WebDriver
。让测试更快速,一致和可靠。 - 视图快照和视频:从命令行运行测试时,我们可以查看失败用例的视图快照和整个测试过程的视频。
除了以上这些功能外,Cypress
还有如下不足之处:
- 不擅长浏览器兼容性测试
- 不擅长微信、微博等
Oauth2.0
授权登录测试 - 只能测试 web 页面(不能测试小程序)
- 尺寸较大,下载安装时间较长
2. 如何安装 Cypress
2.1 下载
因为 Cypress
的体积相对较大(接近 150mb
),所以我们接下来通过浏览器下载安装 Cypress
的过程。
您也可以通过 npm
或 yarn
来安装 Cypress
我们打开 Cypress 官网,点击官网首页的 Download Now 链接开始下载 Cypress
。
下载完成后,我们得到一个名为 cypress.zip
的压缩包文件。
解压缩后,我们会得到一个名为 Cypress
的可执行文件。
2.2 创建工作区
我们在 home
目录下创建一个名为 cypress_demo
的空文件夹(用于存放 cypress 测试文件),作为 Cypress
的工作区。
mkdir $HOME/cypress_demo
我们打开 Cypress
执行文件,会看到如下界面:
点击 select manually
,手动找到并选择我们刚才创建的工作区文件夹 $HOME/cypress_demo
,然后点击 打开
。
这时,我们会看到 Cypress
自动为我们在 cypress_demo
文件夹中生成了一个叫做 cypress
的文件夹和一个叫做 cypress.json
的配置文件。
在 cypress
文件夹中,有 4
个子文件夹,分别是:
fixtures
存放一些测试用例中需要用到的静态资源,比如:数据库模拟数据(json
格式)、图片等信息integration
存放 Cypress 测试文件plugins
存放 Cypress 插件support
存放 Cypress 自定义命令
其中,在 integration
文件夹中,Cypress
还为我们生成了一些测试样例文件,方便我们参考和学习。
我们在 Cypress
的运行界面中可以看到这些样例文件:
完整目录结构如下:
cypress_demo
├── cypress # Cypress 工作目录
│ ├── fixtures # 存放一些测试用例中需要用到的资源,比如:数据库模拟数据、图片、json信息等等
│ │ └── example.json
│ ├── integration # 存放 Cypress 测试文件
│ │ └── examples # 这个文件夹中存放了 Cypress 官方提供的一些测试样例
│ │ ├── actions.spec.js
│ │ ├── aliasing.spec.js
│ │ ├── assertions.spec.js
│ │ ├── connectors.spec.js
│ │ ├── cookies.spec.js
│ │ ├── cypress_api.spec.js
│ │ ├── files.spec.js
│ │ ├── local_storage.spec.js
│ │ ├── location.spec.js
│ │ ├── misc.spec.js
│ │ ├── navigation.spec.js
│ │ ├── network_requests.spec.js
│ │ ├── querying.spec.js
│ │ ├── spies_stubs_clocks.spec.js
│ │ ├── traversal.spec.js
│ │ ├── utilities.spec.js
│ │ ├── viewport.spec.js
│ │ ├── waiting.spec.js
│ │ └── window.spec.js
│ ├── plugins # 存放 Cypress 插件
│ │ └── index.js
│ └── support # 存放 Cypress 自定义命令
│ ├── commands.js
│ └── index.js
└── cypress.json # Cypress 配置文件
2.3 配置 Cypress
接下来,我们来配置 Cypress
,根据上一小节的内容,我们知道,配置 Cypress
,需要通过 cypress.json
这个文件。
那么我们具体能在里面做哪些配置呢?完整的配置内容,请参考官网 配置指南。
这里,我们先关注下面两个配置项:
chromeWebSecurity
userAgent
2.3.1 chromeWebSecurity
chromeWebSecurity
决定是否开启 chrome
浏览器针对同源策略和不安全的混合内容的安全策略。
它默认是开启状态
实际上,这给我们的测试文件内容带来了一些限制,完整的限制名单请参考官网 Web安全限制
这里举个例子,我们在同一个测试用例中分别访问一个主域下的资源,Cypress
允许我们这么做:
cy.visit('https://www.cypress.io')
cy.visit('https://docs.cypress.io') // yup all good
可是,一旦我们在同一个测试用例中,分别访问不同主域下的资源,Cypress
默认会阻止我们:
cy.visit('https://apple.com')
cy.visit('https://google.com') // this will immediately error
为了接近实际的测试场景,我们在 cypress.json
中将它关闭,以方便我们接下来的测试工作:
{
"chromeWebSecurity": false
}
注意:即使我们将 chromeWebSecurity
关闭,Cypress
也依然不允许在同一个测试用例中使用 cy.visit
访问两个不同的顶级域名,详情请参考 #944
2.3.2 userAgent
userAgent
决定 Cypress
访问任何网络资源时,来自哪个操作系统、哪个浏览器、浏览器版本。
这里假设我们平时使用微信开发者工具来开发微信内 h5 页面应用,所以我们将 userAgent
的值设置为微信开发者工具:
{
"chromeWebSecurity": false,
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 wechatdevtools/1.02.1904090 MicroMessenger/6.5.7 Language/zh_CN webview/15602362378809380 webdebugger port/39554"
}
至此,Cypress
的环境配置就完成了。
3. 开发前:如何写基于 Cypress
的测试文件
每个测试文件,都遵循一个模式,大致如下:
- 代表一个测试组的名称和作用域,在
Cypress
中,它叫describe
- 代表整个测试组执行前,进行初始化工作的作用域,在
Cypress
中,它叫before
- 代表整个测试组执行后,进行收尾清理工作的作用域,在
Cypress
中,它叫after
- 代表每个测试用例执行前,进行处理工作的作用域,在
Cypress
中,它叫beforeEach
- 代表每个测试用例执行后,进行处理工作的作用域,在
Cypress
中,它叫afterEach
- 代表一个测试用例的名称和作用域,在
Cypress
中,它叫it
以上这些抽象概念,在 Cypress
中以如下形式展示:
describe('测试组名称', () => {
before(() => {
console.log(' --- 在当前 describe 中所有 it 执行前,运行一次 --- ')
})
after(() => {
console.log(' --- 在当前 describe 中所有 it 执行后,运行一次 --- ')
})
beforeEach(() => {
console.log(' --- 在当前 describe 中每个 it 执行前,运行一次 --- ')
})
afterEach(() => {
console.log(' --- 在当前 describe 中每个 it 执行后,运行一次 --- ')
})
it('测试用例1', () => {
// 在这里写测试逻辑...
})
it('测试用例2', () => {
// 在这里写测试逻辑...
})
})
在开始学习写测试用例的内容之前,我们先思考一下:
开发一个简单的登陆页面,我们平时都是如何测试这个页面的呢?
假设:
- 登录页面的地址为:
http://www.demo.com/login
- 登录成功后,会跳转到地址:
http://www.demo.com/main
- 用户名为:
demo
- 密码为:
password
- 用户名的表单 id 为:
user
- 密码的表单 id 为:
pwd
- 提交按钮的 id 为:
submit
我们平时会手动执行的测试步骤:
- 第一步:在浏览器地址栏输入登陆页面的地址
- 第二步:在登陆页面中,填写用户名和密码,然后点击登录
- 第三步:检查登录后的状态,看是否登录成功
将以上步骤转换为 Cypress
能理解的语言,如下:
describe('登录测试', function () {
it('成功的 case', function() {
// 打开登录页面
cy.visit('http://www.demo.com/login')
// 输入用户名
cy.get('#user').type('demo')
// 输入密码
cy.get('#pwd').type('password')
// 点击提交按钮
cy.get('#submit').click()
// 判断是否登录成功
cy.url().should('include', '/main')
})
})
在 Cypress
中:
- 访问一个网络资源,使用
cy.visit
- 获取网页上某个元素,使用
cy.get
- 向某个表单元素输入内容,使用
cy.type
- 在某个元素上触发点击事件,使用
cy.click
- 获取当前
url
地址,使用cy.url
- 创建一个断言,使用
cy.should
4. 开发中:如何使用 Cypress
测试文件辅助我们进行测试
我们在文件夹 cypress_demo/cypress/integration/
下新建一个名为 demo.js
的文件,将上一小节的代码拷贝进去,然后保存。
接着,让我们回到 2.2
节中打开的 Cypress
执行文件界面(如果您已经关闭了这个界面,只需要重新打开 Cypress
执行文件即可)。
可以看到我们刚才新建的 demo.js
文件了:
点击它,会开始运行测试文件
因为我们还没有开始写这个登录页面,所以运行会出错。
注意:
请在本地 host 文件中将 www.demo.com
指向您的开发机器 ip,或使用您自己的域名;
我们在本篇文章中使用本地 ip: 127.0.0.1
和 www.demo.com
域名
接下来,让我们完成登录页的开发工作,这些工作不在本篇文章的范围内。
在我们完成登录页开发工作后,点击 Cypress
界面的刷新按钮,再次运行测试文件,可以看到,测试已经通过了:
5. 如何更方便得写 Cypress
测试文件
在第 4
节中的例子,实际上是非常简单的
我们实际开发中,可能需要直接测试某个受限资源(需要登录后才能访问),假设我们的登录信息都存储在 cookie
中。
对于表单,除了简单的文字输入,我们也可能需要上传图片(比如:上传头像),假设我们要上传的图片名称为 f.png
,我们需要提前将其拷贝到文件夹 cypress_demo/cypress/fixtures/
中。
甚至我们可能需要为某个前端工程内所有的页面写冒烟测试(smoke testing)。
在 Cypress
中,以上这些通用的功能,都可以通过自定义命令来方便得使用:
cypress_demo
├── cypress
│ └── support
│ ├── commands.js # 在这个文件里手动添加下面的命令内容
我们在 commands.js
中添加我们需要的命令:
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('uploadImage', (fileName, fileType = ' ', selector) => {
cy.get(selector).then(subject => {
cy.fixture(fileName, 'base64')
.then(Cypress.Blob.base64StringToBlob)
.then(blob => {
const el = subject[0]
const testFile = new File([blob], fileName, { type: fileType })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(testFile)
el.files = dataTransfer.files
subject.trigger('change')
})
})
})
Cypress.Commands.add('login', (metaData) => {
Object.keys(metaData).forEach(key => {
cy.setCookie(key, JSON.stringify(metaData[key]))
})
})
Cypress.Commands.add('smoke', selector => {
cy.get(selector).then($app => {
let text = String.prototype.trim.call($app.text())
if (text.length) {
// 冒烟测试的标准为: 页面是否有文本内容
expect(text.length).to.be.gt(0)
} else {
// 或者是否有图片
expect($app.find('img').length).to.be.gt(0)
}
})
})
然后,我们就可以在测试文件中使用这些命令了:
describe('测试组标题', () => {
before(() => {
cy.login({token:'您的token', refreshToken: '您的refreshToken'})
})
it('测试登录后才能访问的资源', () => {
// 在这里写您的测试逻辑
})
it('测试上传图片', () => {
cy.visit('https://www.yourdomain.com/page2')
const fileName = 'f.png'
const fileType = 'image/png'
const uploadFileSelector = 'input[type=file]'
cy.uploadImage(fileName, fileType, uploadFileSelector)
...
})
it('冒烟测试', () => {
let pages = [
'https://www.yourdomain.com/page1',
'https://www.yourdomain.com/page2',
'https://www.yourdomain.com/page2'
]
pages.forEach(page => {
cy.visit(page)
cy.wait(1000)
cy.smoke('body div[id]')
})
})
})
除了自定义命令外,Cypress
还支持请求拦截、截图快照和视频,另外,我们还可以通过插件来扩展 Cypress
。
以上这些功能,本篇文章不再展开,感兴趣的读者可以阅读下面列出的文章。
关于请求拦截,请参考官网 请求拦截器
关于截图快照和视频,请参考官网 截图快照和视频
关于插件,请参考官网 如何写插件
最后,让我们思考下面几个问题:
Cypress
的测试文件,可以反复使用么?- 可以定时运行
Cypress
的测试文件么? - 我们如何利用
Cypress
让我们的工作变得更加轻松?
感谢您花时间阅读这篇文章,希望这篇文章能对您有所帮助。
水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com