[关闭]
@duanyubin 2018-08-29T10:10:29.000000Z 字数 6919 阅读 1414

使用cypress简化测试流程

测试


你是否遇到如下问题:
1. 担心一个页面改动会导致另一个页面挂掉
2. 一直想重构但是没有时间测试,不想一遍遍的走测试用例
3. 改动了多个页面,上线前却又懒得挨个的回归测试,结果上线果然出问题了
4. 想写测试,却又不知如何下手,担心成本大 效果差

如果你遇到上述问题,那就该用cypress搭建一套前端自动化测试流程了。
前端的测试一直都是较为麻烦的,本文探讨如何以最小的成本实现最佳的效果。

本文包含什么:

本文不包含什么:

不覆盖兼容性测试:由于cypress运行在puppeteer之上,所以不能验证兼容性。

cypress

官网https://www.cypress.io,号称Test your code, not your patience.在构建、编写、运行、调试测试等方面都较为简单,提供了等待请求完成、自动化执行输入点击等操作,对每一步操作都可回退查看效果。
vue-cli 3.0也使用了cypress作为测试框架之一。

常见的界面是这样的
cypress界面
左边是测试用例执行,点击每一步可查看当时的截图。
右边是页面内容,可自动执行,输入内容点击按钮。
以下将使用cypress来作为基础工具。

安装与执行

  1. npm i cypress -D
  2. npx run cypress open # 打开GUI界面,能看到测试的每一步执行
  3. npx run cypress run # 命令行执行,有错误会失败,产出截图和视频

安装完毕后项目目录如下

  1. project
  2. |-- 其他文件夹
  3. |-- cypress.json # cypress 配置文件
  4. |-- cypress
  5. |-- fixtures # 放置止mock数据,可以包含js json png等
  6. |-- plugins # 插件,具体的看这里https://on.cypress.io/plugins-guide
  7. |-- integration # 所有的测试用例存放位置,最近常操作的文件夹
  8. |-- screenshots # 错误时的截图
  9. |-- supports
  10. |-- commands.js # 放置自定义指令

测试的一般策略

开发中的测试

E2E测试

最常见也最有效的测试方式。
渲染页面整体,通过测试用例保证DOM结构的正确及部分CSS属性的正确(如display:none),通过肉眼判断布局样式是否正确
一般的流程代码为:

  1. describe('My First Test', function() {
  2. it("Gets, types and asserts", function() {
  3. // 1. 访问页面
  4. cy.visit('https://example.cypress.io')
  5. // 2. 选择dom,并点击
  6. cy.contains('type').click()
  7. // 3. 断言
  8. // 跳转到一个包含'/commands/actions'的url
  9. cy.url().should('include', '/commands/actions')
  10. // 4. 继续添加断言
  11. // 获取input,输入内容并断言input的value
  12. cy.get('.action-email')
  13. .type('fake@email.com')
  14. .should('have.value', 'fake@email.com')
  15. // 5. 可以使用暂停
  16. cy.pause()
  17. // 6. 使用debug
  18. cy.debug()
  19. })
  20. })

通过上述测试用例可以看到,用例没有类似与sleep或waiting的代码,不需要等待操作或 请求的完成,这样大大简化了编码的复杂度。
原理是cypress内部有个循环机制,如果超过默认时间(5s)还没有找到对应的元素,则提示失败。

举例说明:
此处输入图片的描述
所有的数据都是异步ajax获取,需要测试的功能点如下:

  1. 顶部的品牌和车系列表渲染正确
  2. 下方的所有品牌列表加载正确
  3. 图片懒加载,可视区域图片显示,非可视区域不显示,滚动时加载图片
  4. 点击搜索输入框跳转地址
  5. 点击或触摸右侧列表,页面滚动到对应字母位置
  6. 点击品牌列表,弹出品牌详情
  1. beforeEach(() => {
  2. cy.visit('http://localhost:2999')
  3. })
  4. it('渲染基础', () => {
  5. // 1. 顶部的品牌和车系列表渲染正确
  6. cy.get('.brand-list').find('li').should('have.length', 4)
  7. cy.get('.series-list').find('li').should('have.length', 6)
  8. // 2. 所有品牌列表
  9. cy.get('.g-list').find('.item').should('have.length.above', 20)
  10. })
  11. it('图片懒加载', () => {
  12. // 3. 图片懒加载,找到可视区域中的一张图,确保已经存在img标签
  13. cy.get('.series-list')
  14. .find('img').eq(0)
  15. .should('have.attr', 'src')
  16. // 且具有loading-end类
  17. cy.get('.series-list')
  18. .find('.img').eq(0)
  19. .should('have.class', 'loading-end')
  20. cy.get('.js-brand-item').find('.img:first img').should('have.prop', 'src')
  21. // 找到列表中第31个元素,如果正确渲染,它的高度肯定在首屏之外
  22. // 设置别名为hiddenImg,此时应该不存在img标签
  23. cy.get('.js-brand-item .img').eq(30).as('hiddenImg')
  24. cy.get('@hiddenImg').find('img').should('not.exist')
  25. // 滚动页面到此元素
  26. cy.get('@hiddenImg').scrollIntoView()
  27. // 此时应该存在img标签,则表明图片加载完毕
  28. .find('img').should('exist')
  29. })
  30. it('点击搜索跳转', () => {
  31. cy.get('.js-redirect.input').click()
  32. // 4. 要跳转的URL包含search/index,注意跳转路径的测试尽量单独放到一个用例里
  33. cy.url().should('include', '/search/index')
  34. })
  35. it('点击或滑动字母表', () => {
  36. cy.get('.js-category').find('span').should('have.length.above', 10)
  37. // 5. 出发touchstart事件
  38. cy.contains('.js-category span', 'D').trigger('touchstart')
  39. cy.contains('.js-list .title .inner', 'D').then(($element) => {
  40. // 注意通过then方法获取真实的jquery实例
  41. expect($element[0].getBoundingClientRect().y).to.be.lt(4)
  42. })
  43. })
  44. it('点击弹出汽车列表', () => {
  45. cy.get('.js-brand-item:first').click()
  46. // 点击弹出车系列表,visible要求必须在页面可是范围内且不能是display:none的元素。
  47. cy.get('.g-brand-detail').should('be.visible')
  48. // 延迟1s后,关闭弹窗
  49. cy.get('.g-brand-detail .mask').wait(1000).click({ force: true })
  50. cy.get('.g-brand-detail').should('not.be.visible')
  51. })

单元测试

除了常规的e2e测试,单元测试也很重要,尤其是对于一些utils类的工具方法。
单元测试一般的粒度是函数,在函数实现过程中,尽量减少副作用,可简化测试。
但前端经常依赖一些DOM API,常规的单元测试很难编写用例,但是通过cypress就很简单,请看例子:

  1. // 仿zepto的选择器,但返回的是NodeList
  2. export const $ = document.querySelector.bind(document)
  3. export const $$ = selector => Array.prototype.slice.call(document.querySelectorAll(selector))
  4. // 测试用例
  5. it('正常的object', () => {
  6. cy.server()
  7. cy.route('/index.html', `
  8. <ul>
  9. <li>1</li>
  10. <li>2</li>
  11. <li>3</li>
  12. <li>4</li>
  13. </ul>
  14. `)
  15. expect($$('li')).to.be.eq(4)
  16. })

小流量或线上测试

这一步的要求是尽量使用本地测试时的用例,一般是要更改baseUrl和访问的页面地址
配置如下:

  1. // package.json
  2. // 更改baseUrl,且增加环境变量 prod=1
  3. "cypress:online": "CYPRESS_BASE_URL=https://m.dcdapp.com npx cypress open --env prod=1",
  4. // 测试用例中
  5. if (Cypress.env('prod') === 1) {
  6. // 线上地址
  7. cy.visit('/motor/static/brand', { onBeforeLoad })
  8. } else {
  9. // 本地地址
  10. cy.visit('/frontend-test.html', { onBeforeLoad })
  11. }

这样执行npm run cypress:online可对线上的页面进行测试。
小流量的话需要配合charles,手动的配置代理页面地址到小流量机器的IP,目前没有自动配置的方案。

常见问题

如何测试边界情况,如请求为空,status 500等

假设业务有一段代码如下

  1. function fetchData() {
  2. return window.fetch('/fake/data')
  3. .then(res => res.json())
  4. .then((result) => {
  5. if (!result || !result.key) {
  6. return '数据为空'
  7. }
  8. return result
  9. })
  10. .catch(() => '数据异常')
  11. }

很简单的一段获取请求,但其中包含了异常的处理,在编写用例时,需要尽量考虑到这部分的代码。
用例实现如下:

  1. // 先来一段正常逻辑的测试,可以使用mock数据
  2. it('模拟正常数据返回', () => {
  3. cy.server(
  4. // 使用**route**方法,可拦截请求,使用本地数据
  5. cy.route('/fake/data', 'fx:fake.json').as('fetch')
  6. cy.get('.js-button-2.normal').click()
  7. cy.wait('@fetch')
  8. cy.get('#fake')
  9. .should('contain', 'key')
  10. .and('contain', 'value')
  11. })
  12. // 接口返回为空
  13. it('模拟返回数据为空', () => {
  14. cy.server()
  15. cy.route('/fake/data', '{}').as('fetch')
  16. cy.get('.js-button-2.empty').click()
  17. cy.wait('@fetch')
  18. cy.get('#fake')
  19. .should('contain', '数据为空')
  20. })
  21. // 接口错误500
  22. it('模拟接口错误', () => {
  23. cy.server()
  24. cy.route({
  25. url: '/fake/data',
  26. status: 500
  27. }).as('fetch')
  28. cy.get('.js-button-2.error').click()
  29. cy.wait('@fetch')
  30. cy.get('#fake')
  31. .should('contain', '数据异常')
  32. })

如何模拟jsbridge

业务代码如下:

  1. $('.js-delegate').addEventListener(
  2. 'click',
  3. (e) => {
  4. if (e.target.classList.contains('js-button-1')) {
  5. // 执行完jsbridge之后,还有后续的操作
  6. window.ToutiaoJSBridge.call('appInfo', {}, (appInfo) => {
  7. $('#deviceid').textContent = JSON.stringify(appInfo)
  8. })
  9. }
  10. }
  11. }

用例:

  1. it('模拟端的能力', () => {
  2. // 拿到window,注意要在then方法的回调里
  3. cy.window().then((win) => {
  4. // 通过stub方法,代理jsbridge,内容可自定义
  5. cy.stub(win.ToutiaoJSBridge, 'call', (method, _, func) => {
  6. if (method === 'appInfo') {
  7. func({ deviceId: 'deviceid' })
  8. }
  9. })
  10. })
  11. cy.get('.js-button-1').click()
  12. cy.get('#deviceid').should('contain', 'deviceid')
  13. })

使用了window.fetch,目前cypress不支持fetch,如果使用了需要如下操作

  1. before(() => {
  2. // 使用unfetch,强行将window.fetch改成xhr实现
  3. const polyfillUrl = 'http://sf1-ttcdn-tos.pstatp.com/obj/ttfe/motor/lib/unfetch.umd.js'
  4. cy.request(polyfillUrl)
  5. .then((response) => {
  6. polyfill = response.body
  7. })
  8. })
  9. beforeEach(() => {
  10. const onBeforeLoad = (win) => {
  11. /* eslint-disable no-param-reassign */
  12. delete win.fetch
  13. // 执行polyfill代码
  14. win.eval(polyfill)
  15. // 直接替换为xhr版本
  16. win.fetch = win.unfetch
  17. /* eslint-enable */
  18. }
  19. cy.visit('/frontend-test.html', { onBeforeLoad })
  20. })

很多操作有前置要求(如登陆),需要每次都执行前置的操作吗?

当然是不需要,想登陆这种操作,经常被其他操作所依赖,可以将登陆封装为custom command

  1. // cypress/support/commands.js
  2. Cypress.Commands.add('login', (userType, options = {}) => {
  3. // 跳过UI操作,直接发送登陆请求
  4. const types = {
  5. admin: {
  6. name: 'Jane Lane',
  7. password: '123',
  8. admin: true,
  9. },
  10. user: {
  11. name: 'Jim Bob',
  12. password: '123',
  13. admin: false,
  14. }
  15. }
  16. const user = types[userType]
  17. cy.request({
  18. url: '/login',
  19. method: 'POST',
  20. body: {
  21. name: user.name,
  22. password: user.password,
  23. }
  24. })
  25. })
  26. // 使用
  27. cy.login()

下列选择器相关代码报错

  1. // 会报错
  2. const ele = cy.get('#element')
  3. if (ele) {
  4. dosomething()
  5. }
  6. // 这样才是正确的
  7. cy.get('#element').then((ele) => {
  8. dosomething(ele)
  9. })

cypress使用要点

官方的视频很好的介绍了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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注