Cypress 框架的介绍

2021-06-26 148

Cypress 是自集成的,提供了一套完整的端到端测试,无须借助其他外部工具,安装后即可快速地创建、编写、运行测试用例,且对每一步操作都支持回看。

Cypress 简介

  • 基于 JavaScript 的前端测试工具,可以对浏览器中运行的任何内容进行快速、简单、可靠的测试
  • Cypress 是自集成的,提供了一套完整的端到端测试,无须借助其他外部工具,安装后即可快速地创建、编写、运行测试用例,且对每一步操作都支持回看
  • 不同于其他只能测试 UI 层的前端测试工具,Cypress 允许编写所有类型的测试,覆盖了测试金字塔模型的所有测试类型【界面测试,集成测试,单元测试】
  • Cypress 底层协议不采用 WebDriver

Cypress 原理

Webdriver 运行的方式

  • 大多数测试工具(如:Selenium/webdriver)通过在外部浏览器运行并在网络上执行远程命令来运行
  • 因为 Webdriver 底层通信协议基于 JSON Wire Protocol,运行需要网络通信

Cypress 运行的方式

Cypress 和 Webdriver 方式完全相反,它与应用程序在相同的生命周期里执行

Cypress 运行测试的大致流程

  • 运行测试后,Cypress 使用 webpack 将测试代码中的所有模块 bundle 到一个 js 文件中
  • 然后运行浏览器,并且将测试代码注入到一个空白页中,然后它将在浏览器中运行测试代码【可以理解成:Cypress 将测试代码放到一个 iframe 中运行】

Cypress 运行测试的技术流程

  • 每次测试首次加载 Cypress 时,内部 Cypress Web 应用程序先把自己托管在本地的一个随机端口上【如:http://localhost:65874】
  • 在识别出测试中发出的第一个  cy.visit()  命令后,Cypress 会更改本地 URL 以匹配你远程应用程序的 Origin【满足同源策略】,这使得你的测试代码和应用程序可以在同一个 Run Loop 中运行  

Cypress 运行更快的根本原因

  • Cypress 测试代码和应用程序均运行在由 Cypress 全权控制的浏览器中
  • 且它们运行在同一个Domain 下的不同 iframe 中,所以 Cypress 的测试代码可以直接操作 DOM、Window Objects、Local Storages而无须通过网络访问

Cypress 稳定性、可靠性更高的原因

  • Cypress 还可以在网络层进行即时读取和更改网络流量的操作
  • Cypress 背后是 Node.js Process 控制的 Proxy 进行转发,这使得 Cypress 不仅可以修改进出浏览器的所有内容,还可以更改可能影响自动化操作的代码
  • Cypress 相对于其他测试工具来说,能从根本上控制整个自动化测试的流程

Cypress 架构图

Cypress 的特性

时间穿梭【历史记录】

  • Cypress 在测试代码运行时会自动拍照
  • 等测试运行结束后,用户可在 Cypress 提供的 Test Runner 里,通过悬停在命令上的方式查看运行时每一步都发生了什么

实时重新加载

当测试代码修改保存后,Cypress 会自动加载改动地方,并重新运行测试

Spies(间谍)、Stubs(存根)、Clock(时钟)

  • Cypress 允许你验证并控制函数行为,Mock 服务器的响应,更改系统时间
  • 单元测试触手可及!

运行结果一致性

Cypress 架构不使用 Selenium 或 Webdriver,在运行速度、可靠性测试、测试结果一致性上均有良好保障

可调试性

当测试失败时,可以直接从开发者工具(F12 Chrome DevTools)进行调试,这熟悉吧??

自动等待

  • 使用Cypress,永远无须在测试中添加 强制等待、隐性等待、显性等待
  • Cypress 会自动等待元素至可靠操作状态时才执行命令或断言
  • 异步操作触手可及!

网络流量控制

Cypress 可以 Mock 服务器返回的结果,无须依赖后端服务器,即可实现模拟网络请求

截图和视频

Cypress 在测试运行失败时会自动截图,在无头运行时(无GUI界面)会录制整个测试套件的视频

Cypress 优势的总结

归纳起来,具体优点如下:
1. 自集成大部分测试所需的库
像我们在用 Selenium 时,需要集成单元测试框架(unittest、pytest),想要好看的测试报告还得集成(allure),想要 Mock 还得引入对应的 Mock 库而 Cypress 是开箱即用!啥意思?看下图!
2. 执行速度快,方便调试。
3. 适用于中小项目,统一技术栈的团队。
4. 自集成测试截图,很方便。

Cypress 缺点

1. 学习成本过于高,cypress的框架注定只能使用js,且对测试来说js的性价比没有python高,selenium还支持各种语言。
2. 坑坑洼洼太多,cypress在最新的教程都在2020年8月才出,一些莫名的bug和潜在的知识点都无从知晓。
3. cypress使用的是异步调用,不太懂的在这个坑里爬都爬不出。
4. cypress最佳的使用者其实是前端,但一般前端不干这个。
5. 灵活性没selenium高,selenium有太多的库支持其完成骚操作。
6. 对数据的处理没有python好用,js在一些数据处理上会有莫名的问题,不太懂前端就有点尴尬。

默认文件结构

在使用 cypress open 命令首次打开 Cypress,Cypress 会自动进行初始化配置并生成一个默认的文件夹结构,如下图

fixtures 测试夹具

简介

  • 测试夹具通常配合 cy.fixture() 使用
  • 主要用来存储测试用例的外部静态数据
  • fixtures 默认就在 cypress/fixtures 目录下,但也可以配置到另一个目录

外部静态数据的详解

  • 测试夹具的静态数据通常存储在 .json 文件中,如自动生成的 examples.json
  • 静态数据通常是某个网络请求对应的响应部分,包括HTTP状态码和返回值,一般是复制过来更改而不是自己手工填写

fixtures 的实际应用场景

如果你的测试需要对某些外部接口进行访问并依赖它的返回值,则可以使用测试夹具而无须真正访问这个接口(有点类似 mock)

使用测试夹具的好处

  • 消除了对外部功能模块的依赖
  • 已编写的测试用例可以使用测试夹具提供的固定返回值,并且你确切知道这个返回值是你想要的
  • 因为无须真正地发送网络请求,所以测试更快

命令示例

要查看 Cypress 中每个命令的示例,可以打开  cypress/integration/examples ,里面都是官方提供的栗子

test file 测试文件

简介

测试文件就是测试用例,默认位于 cypress/integration ,但也可以配置到另一个目录

测试文件格式

所有在 integration 文件下,且文件格式是以下的文件都将被 Cypress 识别为测试文件
  • .js :普通的JavaScript 编写的文件【最常用啦】
  • .jsx :带有扩展的 JavaScript 文件,其中可以包含处理 XML 的 ECMAScript
  • .coffee :一套 JavaScript 转译的语言。有更严格的语法
  • .cjsx :CoffeeScript 中的 jsx 文件
创建好后,Cypress 的 Test  Runner 刷新之后就可以看到对应测试文件了

plugin file 插件文件

前言

  • Cypress 独有优点就是测试代码运行在浏览器之内,使得 Cypress 跟其他的测试框架相比,有显著的架构优势
  • 这优点虽然提供了可靠性测试,但也使得和在浏览器之外进行通信更加困难【痛点:和外部通信困难】

插件文件的诞生

  • Cypress 为了解决上述痛点提供了一些现成的插件,使你可以修改或扩展 Cypress 的内部行为(如:动态修改配置信息和环境变量等),也可以自定义自己的插件
  • 默认情况,插件位于 cypress/plugins/index.js 中,但可以配置到另一个目录
  • 为了方便,每个测试文件运行之前,Cypress 都会自动加载插件文件 cypress/plugins/index.js

插件的应用场景 

  • 动态更改来自 cypress.json,cypress.env.json,CLI或系统环境变量的已解析配置和环境变量
  • 修改特定浏览器的启动参数
  • 将消息直接从测试代码传递到后端

support file 支持文件

简介

  • 支持文件目录是放置可重用配置项,如底层通用函数或全局默认配置
  • 支持文件默认位于 cypress/support/index.js 中,但可以配置到另一个目录
  • 为了方便,每个测试文件运行之前,Cypress 都会自动加载支持文件 cypress/support/index.js

如何使用支持文件加载钱包

只需要在 cypress/support/commands.ts 文件里配置即可,原生的文件是commands.js,现在将其后缀改为ts。
还配置了一个commands.d.ts。相关代码如下
commands.js中,需要使用三个库:JsonRpcProvider、Wallet、Eip1193Bridge,还需要在index.js中导入commands,如下:
Commands的命令有3个:
Cypress.Commands.add(name, callbackFn)、
Cypress.Commands.add(name, options, callbackFn) 、
Cypress.Commands.overwrite(name, callbackFn)

参数说明

  • name:要添加或覆盖的命令的名称
  • callbackFn :自定义命令的回调函数,回调函数里自定义函数所需完成的操作步骤
  • options:允许自定义命令的隐性行为
如下,在visit函数中注入钱包,因为每次执行用例都会使用visit进入网址。测试用例如下:
commands.ts文件其他代码
// *********************************************** // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** import { JsonRpcProvider } from '@ethersproject/providers' import { Wallet } from '@ethersproject/wallet' import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge' const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY') // address of the above key export const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address export const TEST_ADDRESS_NEVER_USE_SHORTENED = `${TEST_ADDRESS_NEVER_USE.substr( 0, 6 )}...${TEST_ADDRESS_NEVER_USE.substr(-4, 4)}` class CustomizedBridge extends Eip1193Bridge { chainId = 256 async sendAsync(...args) { console.debug('sendAsync called', ...args) return this.send(...args) } async send(...args) { console.debug('send called', ...args) const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function' let callback let method let params if (isCallbackForm) { callback = args[1] method = args[0].method params = args[0].params } else { method = args[0] params = args[1] } if (method === 'eth_requestAccounts' || method === 'eth_accounts') { if (isCallbackForm) { callback({ result: [TEST_ADDRESS_NEVER_USE] }) } else { return Promise.resolve([TEST_ADDRESS_NEVER_USE]) } } if (method === 'eth_chainId') { if (isCallbackForm) { callback(null, { result: '0x100' }) } else { return Promise.resolve('0x100') } } try { const result = await super.send(method, params) console.debug('result received', method, params, result) if (isCallbackForm) { callback(null, { result }) } else { return result } } catch (error) { if (isCallbackForm) { callback(error, null) } else { throw error } } } } // sets up the injected provider to be a mock ethereum provider with the given mnemonic/index Cypress.Commands.overwrite('visit', (original, url, options) => { return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `${url}` : url, { ...options, onBeforeLoad(win) { options && options.onBeforeLoad && options.onBeforeLoad(win) win.localStorage.clear() const provider = new JsonRpcProvider('https://http-testnet.hecochain.com', 256) const signer = new Wallet(TEST_PRIVATE_KEY, provider) win.ethereum = new CustomizedBridge(signer, provider) }, }) })

参考资料


欢迎区块链行业志同道合的小伙伴添加小极微信,加入blockgeek区块链技术交流群,共同推动区块链技术普及和发展~