Vue项目采用Cypress做e2e自动化测试,手把手一撸到底

一、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的可视化界面,会自动打开浏览器


image.png

然后创建一个带cypress测试的初始项目,预设选择手动,功能选e2e testing,配置选择cypress


配置选择cypress

然后点击创建项目,等待安装依赖。(时间会比较久,耐心等待,因为下载cypress比较慢)


image.png

进入这个页面之后,vue ui 可以关掉了。项目已经创建并安装好依赖。

image.png

三、运行示例

  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.
image.png

点击test.js即可看到测试效果


image.png
describe('My First Test', () => {  // 一组测试
  it('Visits the app root url', () => {  //一个测试单元
    cy.visit('/')  //  打开 / 页面
    cy.contains('h1', 'Welcome to Your Vue.js App')  //查找<h1>welcome...</h1>,如果没有找到,会不断重试,直到找到(测试成功)或者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页面

接下来我们要测试这个页面的添删改查功能。


image.png
// 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

image.png

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

image.png

五 其他

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

<div>
<div class='el-image' ><img src='https://xxxx.com/1' /> </div>
<div class='el-image' ><img src='https://xxxx.com/2' /> </div>
</div>

我们要测试第一个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选择器

在编写测试用例的过程中,很多时候我们需要找到元素的唯一选择器,来获取目标元素

image.png

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/",
    }
}

静默执行结果


image.png

六 测试报告

1. dashboard

官方提供了一个在线dashboard,用于查看测试结果,不过免费测试it数只有500个
点击runs,可以获得一串项目码

image.png

image.png

将--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/",
    }
}

image.png

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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342