一、Cypress 介绍
cypress是一款支持现代浏览器的端到端的自动化测试工具。
项目地址 :https://github.com/cypress-io/cypress
目前22.5k star,还是很受欢迎的。
官方文档相当给力,不懂的直接看官方文档更好
cypress安装
https://docs.cypress.io/zh-cn/guides/getting-started/installing-cypress.html
cypress核心概念
如果决定用cypress来做测试,请一定要把这一章从头至尾看完
https://docs.cypress.io/zh-cn/guides/core-concepts/introduction-to-cypress.html
二、vue项目与cypress集成
官方vue-cli
已经提供了相应的plugin:@vue/cli-plugin-e2e-cypress
已有项目集成
根据官方文档,直接执行如下命令即可
npm install -g vue-cli #先安装vue-cli
vue add @vue/e2e-cypress # 再安装插件
可视化创建全新项目集成
由于不熟悉cypress
我选择先通过可视化方式创建全新项目来快速体验
了解vue集成cypress的最小依赖
npm install -g vue-cli #先安装vue-cli
vue ui # 启动ui服务
执行以上命令启动vue-cli的可视化界面,会自动打开浏览器
然后创建一个带cypress测试的初始项目,预设选择手动,功能选e2e testing,配置选择cypress
然后点击创建项目,等待安装依赖。(时间会比较久,耐心等待,因为下载cypress比较慢)
进入这个页面之后,vue ui
可以关掉了。项目已经创建并安装好依赖。
三、运行示例
npm run test:e2e
此命令会先启动vue的服务(http://localhost:8081/)
然后再启动Cypress
DONE Compiled successfully in 8729ms
App running at:
- Local: http://localhost:8081/
- Network: http://192.168.3.114:8081/
App is served in production mode.
Note this is for preview or E2E testing only.
点击test.js即可看到测试效果
describe('My First Test', () => { // 一组测试
it('Visits the app root url', () => { //一个测试单元
cy.visit('/') // 打开 / 页面
cy.contains('h1', 'Welcome to Your Vue.js App') //查找welcome...
,如果没有找到,会不断重试,直到找到(测试成功)或者4秒超时(测试失败)
})
})
四、实战测试
1. 测试目标
接下来我们将测试这个项目 : http://qiniu.veryreader.com/D2CrudPlusExample/
github:https://github.com/greper/d2-crud-plus
面向配置的crud编程,快速开发crud,帮助节省你的时间。
2. 修改启动命令
npm run test:e2e
实际上执行的是vue-cli-service test:e2e
通过给这个命令加--url
参数可以不启动vue项目
直接测试目标url地址
vue-cli-service test:e2e --url http://qiniu.veryreader.com/D2CrudPlusExample/
你可以将此命令配置到package.json里面
"scripts": {
"test:e2e": "vue-cli-service test:e2e",
"test.url:e2e": "vue-cli-service test:e2e --url http://qiniu.veryreader.com/D2CrudPlusExample/"
},
然后执行如下命令即可
npm run test.url:e2e
3. 编写一个登录测试
specs目录下创建login.js
describe('登录', () => {
it('测试登录', () => {
const username = 'admin'
const password = 'admin'
cy.visit('/#/login')
cy.contains('button.button-login', '登录') // 查找button.button-login里包含登录字符串的元素,如果查找不到则失败
cy.get('input[placeholder="用户名"]') //获取input框,写法与jQuery的selector一致
.clear()
.type(username) // input框里面输入用户名
.should('have.value', username) // 断言 input的value=username
// 输入密码
cy.get('input[placeholder="密码"]')//获取input框,写法与jQuery的selector一致
.clear()
.type(password)
.should('have.value', password)
// 提交表单
cy.get('button.button-login').click() //查找按钮,然后点击
cy.contains('首页') //校验是否登录成功
})
})
4. cypress.json配置文件
测试窗口太小,滚动条都出来了,可以按如下修改测试浏览器窗口大小
修改 cypress.json
文件
{
"pluginsFile": "tests/e2e/plugins/index.js",
"viewportWidth": 1920, //窗口宽度
"viewportHeight": 1080 //窗口高度
}
更多配置信息: https://docs.cypress.io/zh-cn/guides/references/configuration.html
5. commond 命令
登录页面是每组测试都要跑一次的,每个测试文件里面都写那么一长串登录代码不现实
supports/commonds.js 中可以将多条测试命令组合成一条命令
Cypress.Commands.add('login', (username = 'admin', password = 'admin') => {
cy.visit('/#/login')
cy.contains('button.button-login', '登录')
cy.get('input[placeholder="用户名"]')
.clear()
.type(username)
.should('have.value', username)
// 输入密码
cy.get('input[placeholder="密码"]')
.clear()
.type(password)
.should('have.value', password)
// 提交表单
cy.get('button.button-login').click()
cy.contains('首页')
})
在测试用例中使用
test.js
describe('My First Test', () => {
it('Visits the app root url', () => {
cy.login('admin', 'admin') //调用 login 命令
})
})
6. before,beforeEach
describe('My First Test', () => {
before(() => {
cy.login()
cy.log('整个describe运行前运行一次,做一些准备工作')
})
beforeEach(() => {
cy.log('每个it之前都会执行,做一些准备工作')
})
it('Visits the app root url', () => {
cy.login('admin', 'admin')
})
afterEach(() => {
cy.log('每个it之后都会执行,做一些清理工作')
})
after(() => {
cy.login()
cy.log('整个describe运行完成后运行一次,做一些清理工作')
})
})
7. 测试一个crud页面
接下来我们要测试这个页面的添删改查功能。
// https://docs.cypress.io/api/introduction/api.html
describe('选择组件', () => {
before(() => {
cy.login() //测试开始前要登录
})
it('打开', () => {
cy.visit('/#/demo/form/select')
cy.wait(1000)
})
it('翻页', () => {
// 翻页
cy.log('翻页')
cy.get('.el-pagination ul.el-pager li').contains('2').click()
cy.checkId(context, '1', false) // 这是一个自定义命令,检查列表第一行id是否不为1
cy.get('.el-pagination ul.el-pager li').contains('1').click()
cy.checkId(context, '1') // 这是一个自定义命令,检查列表第一行id是否为1
})
it('添加', () => {
// 添加
cy.log('添加')
cy.openAdd(context) // 自定义命令,打开添加对话框
// 测试添加对话框里的表单选项
// 找到表单的选择框
cy.formItem('单选远程').find('.el-select').click()
cy.getSelectOptions().first().click() // 点击选择框,并选择第一项
// 点击单选框,选中第一项
cy.formItem('radio').find('.el-radio').first().click()
// 点击保存
cy.closeDialog(context) // 自定义命令,关闭对话框
// 检查是否保存成功
cy.checkId(context, '1', false) // 这是一个自定义命令,检查列表第一行id是否不为1,说明添加成功
// 校验其他列的值是否与添加表单时选的值一致
cy.checkColValue({ col: 2, value: '打开' }) // 校验列中展示的值是否是选择框里的第一项
cy.checkColValue({ col: 10, value: '打开' }) // 校验列中展示的值是否是radio里的第一项
})
it('编辑', () => {
cy.log('编辑')
// 打开编辑对话框
cy.openEdit(context) // 自定义命令,点击第一行的编辑按钮
// 测试表单对话框里的选项
// TODO
// 点击保存
cy.closeDialog(context) // 自定义命令
cy.wait(1000)
})
it('查看', () => {
cy.log('查看')
// 打开编辑对话框
cy.openView(context) // 自定义命令,点击第一行查看按钮
cy.closeDialog(context)// 自定义命令,点击保存
})
it('删除', () => {
cy.log('删除')
cy.doDelete(context) // 自定义命令,点击第一行的删除按钮
})
})
以上包含很多自定义命令,自定义命令封装了很多针对这个项目的通用操作
(只能用在这个项目的测试上,其他项目的命令需要另外自己写)
此处的自定义命令请见:github
8. 动态生成测试(通用骨架)
大部分页面都是crud,每个页面都有打开页面、翻页、添加、修改、删除。
其中只有添加和编辑对话框里的内容和列里面的内容不一样,其他都一样。
根据官方文档 动态生成测试 我们可以在describe
中动态生成it
即可
封装之后我们只需要按如下编写少量的测试代码,即可测试大部分的crud页面了
test2.js
import { createCrudTest } from '../support/creator'
describe('选择组件', () => {
before(() => {
cy.login('admin', 'admin')
})
createCrudTest({
cy,
url: '/demo/form/select',
doAdd () {
//添加对话框要做的事
},
checkAdd () {
//添加成功后的检查,断言列里面的值与添加对话框里面选中的值一致
},
doEdit () {
//编辑对话框要做的事
},
checkEdit(){
//校验编辑是否成功
}
})
})
creator方法太长,请见 github
五 其他
1. 测试稳定性
1.1 断言
https://docs.cypress.io/zh-cn/guides/references/assertions.html
1.2 最佳实践(一定要看)
https://docs.cypress.io/zh-cn/guides/references/best-practices.html
1.3 测试稳定性1
最佳实践中有讲,cy.wait()
是不必要的
目前我的示例里面仍然有cy.wait
说明还有很大改进的空间
并且由于这些cy.wait的存在,使得我的测试用例的成功率变的有点捉摸不定。
所以一定要尽量消除cy.wait
1.4 测试稳定性2
我们要测试第一个img的src要等于https://xxxx.com/1
cy.get('div .el-image img').first().should($el=>{
expect($el.attr('src')).equal('https://xxxx.com/1')
})
其中img的创建是异步的。
当第二个img先创建,第一个img后创建,就会导致获取到第一个img的src=https://xxxx.com/2
然后断言失败
所以结果就是这个测试不稳定,时好时坏。
正确的写法是:先确保两个img都创建好了,再去下src的断言
cy.get('div .el-image img').should('have.length',2).first().should($el=>{
expect($el.attr('src')).equal('https://xxxx.com/1')
})
另外建议尽量少用first() 、last()等方法
first会打断重试链条,一旦first成功进入,之前的部分将不会被重试
2. 文件上传测试
//添加附加文件命令
Cypress.Commands.add(
'attachFile',
{
prevSubject: 'element'
},
(input, fileName, fileType) => {
return cy.fixture(fileName)
.then(content => Cypress.Blob.base64StringToBlob(content, fileType))
.then(blob => {
const testFile = new File([blob], fileName, { type: fileType })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(testFile)
input[0].files = dataTransfer.files
return input
})
}
)
在/tests/e2e/fixtures
放上要上传的文件logo.png
然后使用如下代码即可上传文件
cy.get('input[type=file]')
.attachFile('logo.png', 'image/png')
.trigger('change', { force: true })
3. runner选择器
在编写测试用例的过程中,很多时候我们需要找到元素的唯一选择器,来获取目标元素
4. debugger
cy.get('.xxxxx').then($el=>{ //$el 基本上就是一个jquery对象
debugger //即可进入调试,查看获取到的对象是否正确
})
5. 无头模式执行
给执行命令,添加 --headless参数, 将会不打开GUI,静默运行specs下的所有测试,并生成截图与视频,默认没有测试报告
{
scripts:{
"test.headless:e2e": "vue-cli-service test:e2e --headless --url http://qiniu.veryreader.com/D2CrudPlusExample/",
}
}
静默执行结果
六 测试报告
1. dashboard
官方提供了一个在线dashboard,用于查看测试结果,不过免费测试it
数只有500个
点击runs,可以获得一串项目码
将--record 和 --key 加入到执行命令中,即可将测试结果上传到官方提供的dashboard上。
{
scripts:{
"test.dashboard:e2e": "vue-cli-service test:e2e --record --key 97e8d38c-3824-4cff-ab0d-3d04cbe107c7 --url http://qiniu.veryreader.com/D2CrudPlusExample/",
}
}
2. 报告生成器
cypress也支持本地生成报告并且合并成一个html
https://docs.cypress.io/zh-cn/guides/tooling/reporters.html
下面是mochawesome测试报告的配置过程
1、安装依赖
npm install mocha mochawesome mochawesome-merge mochawesome-report-generator fs-extra -S -D
//或
yarn add mocha mochawesome mochawesome-merge mochawesome-report-generator fs-extra -S -D
2、 配置cypress.json
{
...
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "tests/e2e/results/reports",
"overwrite": false, //配置不覆盖,必须
"html": false,
"json": true, //必须
"toConsole": true
}
}
3、 执行脚本
创建 tests/e2e/report/index.js
const fse = require('fs-extra')
const { merge } = require('mochawesome-merge')
const generator = require('mochawesome-report-generator')
// const cypress = require('cypress')
async function runTests () {
await fse.remove('mochawesome-report')
// await cypress.run({ config: { baseUrl: 'http://localhost:8080/' } })
const options = {
files: [
// you can specify more files or globs if necessary:
'./tests/e2e/results/reports/*.json'
],
reportDir: './tests/e2e/results/'
}
const jsonReport = await merge(options)
// const totalFailed = jsonReport.stats.failures
await generator.create(jsonReport, options)
}
runTests()
4、 添加执行命令
package.json 添加执行命令
"scripts": {
"buildReport": "node tests/e2e/report/index.js"
},
5、 执行命令
# 先执行测试命令
npm run test.headless:e2e
# 再执行报告构建命令
npm run buildReport
报告文件就生成在 tests/e2e/results/mochawesome.html
七 持续集成
https://docs.cypress.io/guides/guides/continuous-integration.html#Examples
cypress在linux服务器上安装比较慢的问题:
设置环境变量即可(仅支持cypress 3.8.3/4.9.0/5.0.0 这三个linux-x64版本)
export CYPRESS_DOWNLOAD_MIRROR=http://www.veryreader.com
echo $CYPRESS_DOWNLOAD_MIRROR
八 一些问题
1. 4.12.1版本的bug
升级cypress到4.12.1之后会持续重复请求静态文件,比如图片,js,css,然后奇慢无比。
issue中提到降级到4.9.0之后此问题消失
更新: 此问题在5.0.0仍没有改善
2. 更换cypress版本
目前官方vue插件对应的cypress版本是3.8.3
我这边改了一些版本
将@vue/cli-plugin-e2e-cypress
替换成如下依赖即可切换cypress版本
@d2-plus/vue-cli-plugin-e2e-cypress:4.5.3-3.8.3
@d2-plus/vue-cli-plugin-e2e-cypress:4.5.3-4.9.0 【没有8.1中说的问题】
@d2-plus/vue-cli-plugin-e2e-cypress:4.5.3-4.12.1
@d2-plus/vue-cli-plugin-e2e-cypress:4.5.3-5.0.0-1
3. linux上运行cypress问题还比较多
1、依赖安装问题(可以通过官方提供的docker-image解决)
2、在docker上运行又会引发共享内存不足的问题(issue上有解决方案)
3、修复内存不足问题之后,还有个运行卡住等问题
九 代码
本文章示例代码:
github: https://github.com/greper/test-cypress
被测项目d2-crud-plus:一个面向配置的crud框架,开发crud就是快
github: https://github.com/greper/d2-crud-plus
帮助文档: http://greper.gitee.io/d2-crud-plus/
示例: http://qiniu.veryreader.com/D2CrudPlusExample/