@duanyubin
2018-08-29T10:10:29.000000Z
字数 6919
阅读 1759
测试
你是否遇到如下问题:
1. 担心一个页面改动会导致另一个页面挂掉
2. 一直想重构但是没有时间测试,不想一遍遍的走测试用例
3. 改动了多个页面,上线前却又懒得挨个的回归测试,结果上线果然出问题了
4. 想写测试,却又不知如何下手,担心成本大 效果差
如果你遇到上述问题,那就该用cypress搭建一套前端自动化测试流程了。
前端的测试一直都是较为麻烦的,本文探讨如何以最小的成本实现最佳的效果。
不覆盖兼容性测试:由于cypress运行在puppeteer之上,所以不能验证兼容性。
官网https://www.cypress.io,号称Test your code, not your patience.在构建、编写、运行、调试测试等方面都较为简单,提供了等待请求完成、自动化执行输入点击等操作,对每一步操作都可回退查看效果。
vue-cli 3.0也使用了cypress作为测试框架之一。
常见的界面是这样的
左边是测试用例执行,点击每一步可查看当时的截图。
右边是页面内容,可自动执行,输入内容点击按钮。
以下将使用cypress来作为基础工具。
npm i cypress -Dnpx run cypress open # 打开GUI界面,能看到测试的每一步执行npx run cypress run # 命令行执行,有错误会失败,产出截图和视频
安装完毕后项目目录如下
project|-- 其他文件夹|-- cypress.json # cypress 配置文件|-- cypress|-- fixtures # 放置止mock数据,可以包含js json png等|-- plugins # 插件,具体的看这里https://on.cypress.io/plugins-guide|-- integration # 所有的测试用例存放位置,最近常操作的文件夹|-- screenshots # 错误时的截图|-- supports|-- commands.js # 放置自定义指令
最常见也最有效的测试方式。
渲染页面整体,通过测试用例保证DOM结构的正确及部分CSS属性的正确(如display:none),通过肉眼判断布局样式是否正确
一般的流程代码为:
describe('My First Test', function() {it("Gets, types and asserts", function() {// 1. 访问页面cy.visit('https://example.cypress.io')// 2. 选择dom,并点击cy.contains('type').click()// 3. 断言// 跳转到一个包含'/commands/actions'的urlcy.url().should('include', '/commands/actions')// 4. 继续添加断言// 获取input,输入内容并断言input的valuecy.get('.action-email').type('fake@email.com').should('have.value', 'fake@email.com')// 5. 可以使用暂停cy.pause()// 6. 使用debugcy.debug()})})
通过上述测试用例可以看到,用例没有类似与sleep或waiting的代码,不需要等待操作或 请求的完成,这样大大简化了编码的复杂度。
原理是cypress内部有个循环机制,如果超过默认时间(5s)还没有找到对应的元素,则提示失败。
举例说明:
所有的数据都是异步ajax获取,需要测试的功能点如下:
beforeEach(() => {cy.visit('http://localhost:2999')})it('渲染基础', () => {// 1. 顶部的品牌和车系列表渲染正确cy.get('.brand-list').find('li').should('have.length', 4)cy.get('.series-list').find('li').should('have.length', 6)// 2. 所有品牌列表cy.get('.g-list').find('.item').should('have.length.above', 20)})it('图片懒加载', () => {// 3. 图片懒加载,找到可视区域中的一张图,确保已经存在img标签cy.get('.series-list').find('img').eq(0).should('have.attr', 'src')// 且具有loading-end类cy.get('.series-list').find('.img').eq(0).should('have.class', 'loading-end')cy.get('.js-brand-item').find('.img:first img').should('have.prop', 'src')// 找到列表中第31个元素,如果正确渲染,它的高度肯定在首屏之外// 设置别名为hiddenImg,此时应该不存在img标签cy.get('.js-brand-item .img').eq(30).as('hiddenImg')cy.get('@hiddenImg').find('img').should('not.exist')// 滚动页面到此元素cy.get('@hiddenImg').scrollIntoView()// 此时应该存在img标签,则表明图片加载完毕.find('img').should('exist')})it('点击搜索跳转', () => {cy.get('.js-redirect.input').click()// 4. 要跳转的URL包含search/index,注意跳转路径的测试尽量单独放到一个用例里cy.url().should('include', '/search/index')})it('点击或滑动字母表', () => {cy.get('.js-category').find('span').should('have.length.above', 10)// 5. 出发touchstart事件cy.contains('.js-category span', 'D').trigger('touchstart')cy.contains('.js-list .title .inner', 'D').then(($element) => {// 注意通过then方法获取真实的jquery实例expect($element[0].getBoundingClientRect().y).to.be.lt(4)})})it('点击弹出汽车列表', () => {cy.get('.js-brand-item:first').click()// 点击弹出车系列表,visible要求必须在页面可是范围内且不能是display:none的元素。cy.get('.g-brand-detail').should('be.visible')// 延迟1s后,关闭弹窗cy.get('.g-brand-detail .mask').wait(1000).click({ force: true })cy.get('.g-brand-detail').should('not.be.visible')})
除了常规的e2e测试,单元测试也很重要,尤其是对于一些utils类的工具方法。
单元测试一般的粒度是函数,在函数实现过程中,尽量减少副作用,可简化测试。
但前端经常依赖一些DOM API,常规的单元测试很难编写用例,但是通过cypress就很简单,请看例子:
// 仿zepto的选择器,但返回的是NodeListexport const $ = document.querySelector.bind(document)export const $$ = selector => Array.prototype.slice.call(document.querySelectorAll(selector))// 测试用例it('正常的object', () => {cy.server()cy.route('/index.html', `<ul><li>1</li><li>2</li><li>3</li><li>4</li></ul>`)expect($$('li')).to.be.eq(4)})
这一步的要求是尽量使用本地测试时的用例,一般是要更改baseUrl和访问的页面地址
配置如下:
// package.json// 更改baseUrl,且增加环境变量 prod=1"cypress:online": "CYPRESS_BASE_URL=https://m.dcdapp.com npx cypress open --env prod=1",// 测试用例中if (Cypress.env('prod') === 1) {// 线上地址cy.visit('/motor/static/brand', { onBeforeLoad })} else {// 本地地址cy.visit('/frontend-test.html', { onBeforeLoad })}
这样执行npm run cypress:online可对线上的页面进行测试。
小流量的话需要配合charles,手动的配置代理页面地址到小流量机器的IP,目前没有自动配置的方案。
假设业务有一段代码如下
function fetchData() {return window.fetch('/fake/data').then(res => res.json()).then((result) => {if (!result || !result.key) {return '数据为空'}return result}).catch(() => '数据异常')}
很简单的一段获取请求,但其中包含了异常的处理,在编写用例时,需要尽量考虑到这部分的代码。
用例实现如下:
// 先来一段正常逻辑的测试,可以使用mock数据it('模拟正常数据返回', () => {cy.server(// 使用**route**方法,可拦截请求,使用本地数据cy.route('/fake/data', 'fx:fake.json').as('fetch')cy.get('.js-button-2.normal').click()cy.wait('@fetch')cy.get('#fake').should('contain', 'key').and('contain', 'value')})// 接口返回为空it('模拟返回数据为空', () => {cy.server()cy.route('/fake/data', '{}').as('fetch')cy.get('.js-button-2.empty').click()cy.wait('@fetch')cy.get('#fake').should('contain', '数据为空')})// 接口错误500it('模拟接口错误', () => {cy.server()cy.route({url: '/fake/data',status: 500}).as('fetch')cy.get('.js-button-2.error').click()cy.wait('@fetch')cy.get('#fake').should('contain', '数据异常')})
业务代码如下:
$('.js-delegate').addEventListener('click',(e) => {if (e.target.classList.contains('js-button-1')) {// 执行完jsbridge之后,还有后续的操作window.ToutiaoJSBridge.call('appInfo', {}, (appInfo) => {$('#deviceid').textContent = JSON.stringify(appInfo)})}}}
用例:
it('模拟端的能力', () => {// 拿到window,注意要在then方法的回调里cy.window().then((win) => {// 通过stub方法,代理jsbridge,内容可自定义cy.stub(win.ToutiaoJSBridge, 'call', (method, _, func) => {if (method === 'appInfo') {func({ deviceId: 'deviceid' })}})})cy.get('.js-button-1').click()cy.get('#deviceid').should('contain', 'deviceid')})
before(() => {// 使用unfetch,强行将window.fetch改成xhr实现const polyfillUrl = 'http://sf1-ttcdn-tos.pstatp.com/obj/ttfe/motor/lib/unfetch.umd.js'cy.request(polyfillUrl).then((response) => {polyfill = response.body})})beforeEach(() => {const onBeforeLoad = (win) => {/* eslint-disable no-param-reassign */delete win.fetch// 执行polyfill代码win.eval(polyfill)// 直接替换为xhr版本win.fetch = win.unfetch/* eslint-enable */}cy.visit('/frontend-test.html', { onBeforeLoad })})
当然是不需要,想登陆这种操作,经常被其他操作所依赖,可以将登陆封装为custom command
// cypress/support/commands.jsCypress.Commands.add('login', (userType, options = {}) => {// 跳过UI操作,直接发送登陆请求const types = {admin: {name: 'Jane Lane',password: '123',admin: true,},user: {name: 'Jim Bob',password: '123',admin: false,}}const user = types[userType]cy.request({url: '/login',method: 'POST',body: {name: user.name,password: user.password,}})})// 使用cy.login()
// 会报错const ele = cy.get('#element')if (ele) {dosomething()}// 这样才是正确的cy.get('#element').then((ele) => {dosomething(ele)})
官方的视频很好的介绍了cypress的特点
https://youtu.be/5XQOK0v_YRE
DOM操作: https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html
API文档: https://docs.cypress.io/api/introduction/api.html
网络相关:https://docs.cypress.io/guides/guides/network-requests.html
断言写法:https://docs.cypress.io/guides/references/assertions.html
使用stub、spy、clock进行单元测试:https://docs.cypress.io/guides/guides/stubs-spies-and-clocks.html