基于Cypress.js和Chrome插件的UI自动化测试平台实践

1,319 阅读16分钟

背景

  • 质量第一,稳定压倒一切

  • 当下项目的重构接近尾声,大部分模块已在线上环境运行一段时间了。对一些基础核心业务,部分逻辑甚少发生变化的功能模块的稳定性是开发和测试需要考虑的问题

  • 日常业务项目开发的痛点之一便是前端的回归测试,免不了各种手动点点点,但凡改动了某个公用组件,函数,都要漫山遍野地把项目的主要页面都点进去看一遍有没有问题。目前项目产品迭代发版频繁,每次发版对回归测试验证工作是比较大的,消耗大量人力还不能完全保证回归测试100%场景覆盖

  • 相较于接口自动化测试,Web UI自动化测试面临着应用程序界面的复杂性和稳定性的诸多挑战,想建设起来完整的UI自动化测试需要更多人力和技术的投入

  • 依托于前端Chrome插件开发能力和现有ui自动化测试框架的结合,将自动化脚本可视化管理,来降低自动化测试的编写难度和维护成本

前言

因为本身对web自动化测试就比较感兴趣,恰巧今年项目在发版过程中遇到多次很明显的问题发到线上导致版本回滚, 这些事故完全可以通过UI自动化测试来提前发现避免一些很明显的bug带到线上去。所以考虑在现有开发联调小工具插件中加UI测试的功能模块,从前端角度探索web ui自动化测试的可行性实践。探索出一个自动化测试雏形解决方案

UI自动化框架选择

目前市面上已存在了很多 UI 自动化测试库和解决方案,百花齐放

  1. Selenium (多语言)
  2. webdriver.io (nodejs)
  3. Cypress (nodejs)
  4. karate (java)
  5. macaca (多语言)
  6. Puppeteer
  7. Nightwatch
  8. ...
PuppeteerSeleniumwebdriver.ioCypress
支持浏览器Chrome/Chromium多种浏览器多种浏览器Chrome/Electron/Firefox
测试集成无(不具备测试功能)自带测试报告需要借助三方工具自带自带且测试报告丰富(自带视频,截图,测试文档形式)
学习难度
语言JavaScriptJava/Python/C#/Ruby/JavaScript/KotlinJavaScriptJavaScript
使用情况字节飞书团队(Jest+Puppeteer)京东拼购团队快手主站快手商业化谷歌微软奈飞字节跳动Data团队携程度假团队腾讯文档团队
社区活跃star: 74.7kstar:22.1kstar:7.1kstar:34.7k

方案虽然很多,不过合适的才是最好的,目前根据 我们项目的实际情况 ,针对 我们项目现在的现状和期望:

  • 覆盖主流程功能测试用例
  • 测试用例易编写,学习成本低

最终采用的是 Cypress,Cypress 的测试代码语法足够的简单且用javascript写脚本,前端同学上手无学习成本

Cypress.js自动化框架

什么是cypress

Cypress 是非常年轻但很受开发者欢迎的测试框架,本地开发的话仅需要nodejs环境不需要安装别的依赖,npm install Cypress 即可,开箱即用,对于ES6 ES7的语法天然支持,不仅支持本地浏览器直接模拟测试,也支持终端测试。还有测试录屏功能,方便在测试失败的时候,查看当时的失败的场景,方便修改。整体来说上手快,学习成本较低

cypress安装与使用

使用cypress需要电脑先安装node.js环境

  • 创建一个空目录(my-cypress-demo),通过npm init -y 命令初始化创建package.json文件
// 打开目录
cd my-cypress-demo
// 初始化创建package.json文件
npm init -y
  • 通过npm安装cypress
npm install cypress --save-dev
  • 这个安装命令会在当前目录下自动创建如下内容:
|-- cypress.config.js    // 配置文件
|-- cypress
    -- fixtures     // 用于存放自定义的json文件
    -- e2e  // 测试代码
    -- plugins       // 自定义指令时,与support文件夹组合使用
        -- index.js
    -- support
        -- commands.js
        -- index.js
  • package.json中添加Cypress的启动命令
{
    "scripts": {
        "cypress:open": "cypress open"
    }
}
  • 启动时直接使用如下命令:
npm run cypress:open

至此已经把一个cypress.js的自动化工程搭建起来了,自动创建的cypress/e2e目录下有官方测试用例示例,可以直接运行启动命令体验一下。

cypress优缺点

优点
  1. 简单易用:Cypress的API直观易懂,降低了学习曲线。
  2. 高性能:实时重载和WebSocket技术提高了测试执行速度和调试效率。
  3. 功能丰富:支持自动等待、截图、视频录制、网络请求监听等。
  4. 实时反馈:测试运行时提供实时步骤反馈,便于调试。
  5. 时间旅行:支持回放测试步骤,帮助深入理解测试过程。
  6. 易于集成:轻松集成到CI/CD流程中,支持自动化部署。
  7. 活跃社区:拥有活跃的社区和丰富的资源支持。
缺点
  1. 语言限制:仅支持JavaScript,可能限制非JS开发者的使用。

  2. 浏览器支持有限:虽然支持多浏览器,但主要还是对Chrome和Electron的支持。

  3. 不支持多标签页:无法在多标签页或多窗口中执行测试。

Cypress.js编写测试用例

用官方文档的一个例子说明一下测试代码怎么写:(cypress/e2e/1-getting-started/todo.cy.js)

describe('example to-do app', () => {
  beforeEach(() => {
    cy.visit('https://example.cypress.io/todo')
  })

  it('displays two todo items by default', () => {
    cy.get('.todo-list li').should('have.length', 2)
    cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
    cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
  })

  it('can add new todo items', () => {
    const newItem = 'Feed the cat'
    cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
    cy.get('.todo-list li')
      .should('have.length', 3)
      .last()
      .should('have.text', newItem)
  })
})

上面测试代码简洁语义化较好,代码量不多,也不需要写很多逻辑,上手比较简单。

一些常用Api代码示例

describe('Cypress 常用API 示例', () => {  
  
    // 访问页面  
    it('访问 Cypress 官网', () => {  
        cy.visit('https://www.cypress.io')  
        cy.title().should('include', 'Cypress') // 断言页面标题包含 "Cypress"  
    })  
  
    // 元素查找与交互  
    it('在 Cypress 官网中查找并点击 "Features" 链接', () => {  
        cy.visit('https://www.cypress.io')  
        cy.contains('a', 'Features').click() // 查找包含 "Features" 的链接并点击  
        cy.url().should('include', '/features') // 断言URL中包含 "/features"  
    })  
  
    // 表单操作  
    it('在表单中填写并提交数据', () => {  
        cy.visit('https://example.cypress.io/commands/actions') // 假设这是一个包含表单的测试页面  
        cy.get('#email').type('test@example.com') // 在邮箱输入框中输入文本  
        cy.get('#submit').click() // 点击提交按钮  
        cy.url().should('not.include', '/commands/actions') // 断言提交后URL发生变化  
    })  
  
    // 断言  
    it('断言页面元素的存在与文本内容', () => {  
        cy.visit('https://www.cypress.io')  
        cy.get('h1').should('exist') // 断言页面中的h1标签存在  
        cy.get('h1').should('contain', 'Fast, Easy and Reliable Testing for Anything that Runs in a Browser') // 断言h1标签包含特定文本  
    })  
  
    // 循环与条件  
    it('遍历页面上的多个元素', () => {  
        cy.visit('https://example.cypress.io/commands/iterations-and-aliases') // 假设页面上有多个可迭代元素  
        cy.get('.item').each(($el, index, $list) => {  
            // 对每个元素执行操作,例如打印其文本  
            cy.wrap($el).invoke('text').then(text => {  
                console.log(`Item ${index + 1}: ${text}`)  
            })  
        })  
    })  
  
    // 等待与重试  
    it('等待元素加载完成', () => {  
        cy.visit('https://example.cypress.io/commands/waiting')  
        cy.get('.slow-loading-element', { timeout: 10000 }).should('be.visible') // 等待元素在10秒内变得可见  
    })  
  
    // 网络请求拦截  
    it('拦截并验证网络请求', () => {  
        cy.server() // 开启cy.route()和cy.stub()的拦截功能  
        cy.route('GET', '/some/api/endpoint').as('getApi') // 拦截GET请求并命名为'getApi'  
        cy.visit('https://example.cypress.io/commands/network-requests')  
        cy.wait('@getApi').then((xhr) => { // 等待名为'getApi'的请求完成  
            expect(xhr.status).to.eq(200) // 断言HTTP状态码为200  
        })  
    })  
})

Cypress 的 API 设计得非常直观和强大,能够处理各种复杂的测试场景。通过充分利用这些 API,开发者可以编写出既易于维护又高效的测试代码,从而提高应用的质量和稳定性。

  1. 访问和操作网页

  • cy.visit(url): 加载并访问指定的 URL。

  • cy.go(backOrForward): 在浏览器历史记录中向前或向后导航。

  • cy.reload(): 重新加载当前页面。

  • cy.location(): 获取并操作当前页面的 URL、路径、查询参数等。

  1. DOM 元素查询和操作

  • cy.get(selector): 查询并返回匹配指定选择器的 DOM 元素。

  • cy.contains(content): 查询包含特定文本或子元素的 DOM 元素。

  • cy.check(selector): 选中复选框。

  • cy.uncheck(selector): 取消选中复选框。

  • cy.select(selector, option): 从下拉列表中选择一个选项。

  • cy.type(text): 在可编辑的元素中输入文本。

  • cy.clear(): 清除可编辑元素中的内容。

  • cy.click(selector): 点击元素。

  1. 等待和重试

  • cy.wait(ms): 等待指定的毫秒数。

  • cy.wait(alias): 等待一个通过 cy.route()cy.intercept() 拦截的网络请求完成。

  • cy.should(chainer): 对 DOM 元素或其他对象执行断言,并在不满足条件时重试。

  1. 断言

  • cy.should(chainer): 对当前命令链的结果执行断言。

  • cy.and(chainer): 链式调用多个断言。

  • cy.not(chainer): 反向断言,即断言某条件不成立。

  1. 网络请求

  • cy.intercept(url, options): 拦截和模拟网络请求。这是 Cypress 7.0 及更高版本中用于替代 cy.route()cy.server() 的新方法。

  • cy.wait(alias): 等待由 cy.intercept() 拦截的请求完成。

  1. 文件上传

  • cy.fixture(filePath): 加载测试夹具(fixture)文件,常用于模拟文件上传。

  • cy.get(selector).attachFile(filePath): 将文件附加到文件输入元素上,模拟文件上传。

  1. 钩子(Hooks)

  • before(), beforeEach(), after(), afterEach(): 在测试运行的不同阶段执行代码,如设置前置条件、清理环境等。

  1. 工具函数

  • Cypress.env(variable): 访问 Cypress 配置文件或环境变量中定义的变量。

  • Cypress.log(): 记录自定义日志消息,这对于调试和构建复杂的测试非常有用。

  1. 插件和扩展

Cypress 还支持通过插件和扩展来增强其功能,比如添加新的命令、断言或报告器等。

Page Object自动化测试模式

Page Object(页面对象)自动化测试模式是一种在UI自动化测试中广泛使用的设计模式,其核心思想是将页面的元素和操作封装成对象,以提高测试代码的可维护性、可重用性和可读性。

在Cypress.js中,Page Object模式同样是一种非常有用的设计模式,用于提高自动化测试的可维护性、可重用性和可读性。尽管Cypress.js已经通过其命令链(如cy.get(), cy.click()等)提供了很好的抽象和封装,但将页面的元素和操作进一步封装到自定义的Page Object类中,可以进一步促进代码的模块化和组织。

如何在Cypress.js中使用Page Object模式

  • 定义Page Object类

首先,你需要在Cypress项目中定义Page Object类。这些类将包含针对特定页面的元素定位和操作。你可以选择在Cypress的cypress/support目录下创建一个新的文件夹(如pageObjects),并在其中定义你的Page Object类

// cypress/support/pageObjects/loginPage.js  
class LoginPage {  
    constructor() {  
        this.usernameInput = 'input[name="username"]';  
        this.passwordInput = 'input[name="password"]';  
        this.loginButton = 'button[type="submit"]';  
    }  

    typeUsername(username) {  
        cy.get(this.usernameInput).type(username);  
    }  

    typePassword(password) {  
        cy.get(this.passwordInput).type(password);  
    }  

    clickLogin() {  
        cy.get(this.loginButton).click();  
    }  
}  

export default LoginPage;
  • 在测试文件中使用Page Object

在你的测试文件中,你可以导入并使用这个Page Object类。这样做的好处是,你的测试文件将变得更加简洁,并且只关注测试逻辑,而不是页面元素的具体实现。

// cypress/integration/login_spec.js  
import LoginPage from '../support/pageObjects/loginPage';  

describe('Login Page', () => {  
    it('should login successfully', () => {  
        const loginPage = new LoginPage();  

        loginPage.typeUsername('user123');  
        loginPage.typePassword('pass123');  
        loginPage.clickLogin();  

        // 假设登录后会被重定向到仪表盘页面  
        cy.url().should('include', '/dashboard');  
    });  
});

集成mochawesome生成测试报告

  • 安装Mochawesome和Cypress Mochawesome Reporter
npm install --save-dev mochawesome mochawesome-report-generator cypress-multi-reporters
  • 修改cypress.config.js配置项
const { defineConfig } = require("cypress");
module.exports = defineConfig({
  e2e: {
    testIsolation: false
  },
  reporter: "mochawesome", //配置报告类型
  reporterOptions: {
    reportDir: "cypress/results/mochawesome-report",
    overwrite: false, // 如果设置为true,则每次测试运行时都会覆盖之前的报告
    html: true, // 生成 HTML 报告
    json: true, // 生成 JSON 报告
  }
});

自定义cypress.config.js配置

在Cypress中,cypress.config.js配置文件允许你自定义多个配置项,以满足你的测试需求。这些配置项涵盖了测试运行、环境变量、浏览器设置、截图和视频录制等多个方面。以下是一些常见的配置项及其说明

以下是一个cypress.config.js配置文件的示例

const { defineConfig } = require('cypress/types/config')  
  
module.exports = defineConfig({  
  projectId: 'your-project-id',  
  baseUrl: 'http://localhost:3000',  
  env: {  
    apiKey: 'your-api-key'  
  },  
  integrationFolder: 'cypress/tests',  
  fixturesFolder: 'cypress/fixtures',  
  pluginsFile: 'cypress/plugins/index.js',  
  supportFile: 'cypress/support/index.js',  
  viewportWidth: 1280,  
  viewportHeight: 720,  
  chromeWebSecurity: false,  
  screenshotsFolder: 'cypress/screenshots',  
  videosFolder: 'cypress/videos',  
  screenshotOnRunFailure: true,  
  video: false,  
  defaultCommandTimeout: 10000,  
  pageLoadTimeout: 60000,  
  requestTimeout: 5000  
  // 注意:关于报告生成器的配置,请在cypress/plugins/index.js中处理  
})

以上就是Cypress.js框架的基础概念和简单的使用,总结一下就是:

1、cypress.js工程搭建非常简单,通过npm一行命令就能初始化好工程,并给你生成好了示例代码

2、参照示例代码编写我们的用例脚本,cypress.js提供了丰富的api让我们能实现各种场景自动化测试代码

3、为了更好的代码维护,引入Page Object自动化测试模式来组织我们的测试用例脚本代码

4、cypress.js没有内置测试报告,所以需要安装个mochawesome插件来输出测试报告

5、cypress.js还提供了更多灵活的配置,可以在cypress.config.js里添加相关配置项(详细参数见官网)


如果我们想把工程里的测试用例脚本通过后端接口服务存储到数据库中,然后通过前端管理页面可视化管理,多人协作维护用例的增删改查,就需要把测试用例脚本做转换成JSON格式存储到线上数据库(Mongodb/MySQL)中,在需要运行自动化测试时通过ejs模版引擎再把JSON数据转换成javascript脚本来执行。

Case数据转换

  • 先看一个用例集的脚本结构,我们可以把它用json的形式表示成如下
describe('某功能模块', () => {  //测试集
  before(()=>{
    //在所有测试用例执行前执行
  })
  beforeEach(()=>{
    //在每一个测试用例执行前执行
  })
  after(()=>{
    //在所有测试用例执行完后执行
  })
  afterEach(()=>{
    //在每一个测试用例执行后执行
  })
  it('增!', () => {  //测试用例
    //测试用例内容
  })
  it('删', () => {  //测试用例
    //测试用例内容
  })
  it('改', () => {  //测试用例
    //测试用例内容
  })
  it('查', () => {  //测试用例
    //测试用例内容
  })
……
})
{
  name: '某功能模块',
  before: [],
  beforeEach: [],
  after: [],
  afterEach: [],
  it: [
    {
      name: '增!',
      //测试用例内容
      commands: []
    }
    {
      name: '删',
      //测试用例内容
      commands: []
    }
    ...
  ]
}

上面可以看出一个测试用例集JavaScript脚本的代码结构还是挺清晰的、有迹可循的,很好用json来表示

  • 再把操作指令转换成json的形式表示

先看几个常用操作指令代码示例

// 点击
cy.get('#action').click()
// 输入
cy.get('.action-clear').type('Clear this text')
// 鼠标hover
cy.get('#action').trigger('mouseenter')
// 断言
cy.get('.action').should('have.value', 'hw')
cy.get('.action').should('contain', 'hw')
// 等待元素可见
cy.get('.action').should('be.visible')

可以把上面的指令归纳成:操作类型 + 一系列的参数 。所以我们可以把上面的每一个操作指令表示成如下结构:

用json来表示就是:

{
    type'',
    argument1: '',
    argument2: '',
    argument3: ''
}

比如上面的点击事件cy.get('#action').click()就可以用json表示为

{type:'click', argument1: '#action', argument2: '', argument3: ''}

那么我们就可以做一个页面来编辑json生成测试脚本, 由界面管理生成json数据,再由json数据生成脚本代码

操作指令ejs模版转换示例:

这样我们就可以实现用json的形式表示一个测试用例脚本,通过数据的形式管理脚本json,需要使用时又可以通过ejs模版将json转换成JavaScript脚本文件,提供cypress.js执行

有了以上的用例数据转换的基础我们就可以开发一个前端管理页面,通过界面来可视化增删改查cypress.js测试用例脚本了,即UI自动化测试平台

UI自动化平台架构方案

该平台包含3个部分: 1、前端管理页面,提供界面快捷操作增删改查用例集|用例

2、服务端接口服务,连接数据库保存用例数据

3、cypress.js集成,当页面上发起自动化测试请求时,cypress.js从服务端拿测试用例数据,通过ejs模版转换成javascript脚本文件,再启动程序执行这些脚本文件进行自动化测试并生成报告

具体前后端项目工程搭建与技术实现参加另外两文档:

UI自动化测试平台工程搭建

基于Chrome插件的测试联调小工具开发实践

基于Chrome插件的测试脚本管理

为了更方便的编写和调试测试用例代码,我们可以把测试用例管理的前端页面通过chrome插件添加到页面调试面板上去,这样就可以边操作页面元素,边维护自动化测试用例,还可以通过chrome调试面板的元素查看页面的HTML结构。借助于chrome插件提供的能力,可以直接和页面进行互动,更方便的调试测试用例,和检测用例是否执行符合预期

自动化测试平台界面功能:

  • 项目管理

对项目的新增,修改,删除,添加协作人

  • 用户管理

用户成员的增删改查,重置密码,权限设置等

  • 测试用例管理
  1. 测试用例集(建议以页面为单位)增、删、改、查、排序
  2. 测试用例集的指令集增、删、改、查、排序
  3. 用例集设置,添加before(), beforeEach(), after(), afterEach() 等hooks编辑
  4. 测试用例增、删、改、查、排序,步骤可视化执行验证,快速复制粘贴,导出cypress.js脚本
  • 测试用例脚本有效性检测

依托于chrome插件可以直接操作页面html元素, 可以在用例详情页面模拟执行,可视化校验用例运行是否符合预期

  • 指令集管理

将一些公共的操作步骤封装成一个指令,其他地方直接复用,提高用例编辑的灵活性。

  • 创建测试单

创建测试单需要先获取到当前项目测试页面地址的cookie保存起来,是绕过登录的关键步骤

支持自定义选择需要执行的测试用例

创建完测试单,点击执行就开始在服务端自动运行自动化测试程序,运行结束生成测试报告

  • 测试报告

测试报告提供两种模式

1、列表表格形式展示测试结果,并关联测试失败时的截图,可以快速查看执行情况,和失败用例执行截图

2、标准mochawesome自动化测试报告HTML输出

image.png

生成自动化测试用例脚本文件

本地跑自动化测试时需要从数据库把测试用例数据生成可执行的JavaScript代码,提供给cypress.js执行

借助ejs模版能力可以轻松完成生成自动化脚本文件的操作,先把ejs模版定义好 app/generate/ejs/case.cy.ejs

然后通过ejs提供的render方法生成js文件

  let ejsCasePath = path.resolve(pathConfig.ejs, "case.cy.ejs");
  
  // 读取模板文件
    fs.readFile(ejsCasePath, "utf8")
      .then((template) => {
        // 使用EJS渲染模板(返回Promise)
        return ejs.render(template, caseData);
      })
      .then((result) => {
          // 处理模版中的一些特殊字符
        result = result
          .replace(/'/g, "'")
          .replace(/"/g, '"')
          .replace(/>/g, ">");
        result = removeEmptyLines(result);
      })
      .catch((err) => {
        console.log(err);
      });

生成的js文件代码

理论上我们可以通过上面的转换方式把测试用例转换成任意编程语言或测试框架的代码,只要写好对应的ejs模版即可。所以在自动化测试平台中,cypress.js只是个测试框架的载体,所以可以替换成任意其他的自动化测试框架来实现测试

自动化平台功能演示

省略...

E2E 测试的痛点

稳定性和可维护性

文章参考

www.cypress.io/how-it-work…

ToB 业务场景下自动化测试的实践及探索

基于数据沙箱与LLM用例自愈的UI自动化测试平台

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。