关于前端:前端ui自动化测试sdk封装

197次阅读

共计 9258 个字符,预计需要花费 24 分钟才能阅读完成。

背景

前端业务场景中每次性能公布都会面临着相应的 ui 功能测试,因为前端业务的性能迭代之间往往存在显性或者隐性的关联性,每次上线某个性能迭代后,严格意义上也须要对整体性能进行回归,因而单靠人力的手工测试须要破费较多的工夫和精力在性能回归上,且容易漏掉一些细节问题。
基于业务中的上述现状,咱们尝试引入 ui 自动化测试来解决测试中的“反复回归”问题,基于 puppeteer 和 jest 两大开源工具,封装了一款 UI 自动化测试 sdk,实用于以下两个常见业务场景:

  • 稳固的老业务,性能不常常迭代,通过自动化测试实现每次公布的测试
  • 正在疾速迭代中业务中的外围流程,通过自动化测试保障每次公布后外围流程的性能失常

性能阐明

  1. 通过 sdk 和配置文件,主动实现 ui 自动化测试流程
  2. 反对浏览器实时复现整个测试过程
  3. 反对 pc 和 h5 不同终端的测试
  4. 主动生成测试流程中的性能页面截图,用户自行通过截图进行测试后果断定
  5. 反对预设页面性能的比对图片,主动实现页面截图和预设比对图片的比对,依据比对差别进行测试后果断定
  6. 主动输入最终的测试报告
  7. 反对全局接口的监控,反对自定义接口测试
  8. 同时反对 es 和 cjs 的输入,兼容 import 和 require 的导入
  9. 测试过程中命令行终端中实时输入测试停顿

应用形式

第一步:前端工程中引入

通过 npm 的形式,装置好 sdk

npm i xxx
// 这里须要留神的是,肯定不要用原始的 npm 镜像源,因为包外面的 chromium 的源地址在国外,下载会失败,能够用 cnpm、taobao 镜像源等代理镜像源 

第二步:在工程中创立测试入口文件,举荐的文件目录如下

 工程根目录
    |
    +---uiTest
        |       
        +---origin // 如果须要图片比对,寄存原始的比对图片的目录
        |       xxx.png
        |       xxx.png
        |       
        +---result // 寄存测试过程中截图和最终图片比对后果图的目录
        |       xxx.png
        |       xxx.png
        |
        +---test.js // 测试入口文件 

第三步:在测试入口文件 test.js 中接入 sdk

const UITestPlayer = require('xxx');
const myUITestPlayer = new UITestPlayer({
  headless: false,
  fullScreen: true
});
myUITestPlayer.run(runConfig);

若应用 import 形式,则须要保障以后工程的 package.json 中具备 type:module 字段,或者创立的入口文件的后缀是.mjs(test.mjs)

import UITestPlayer from 'xxx';
const myUITestPlayer = new UITestPlayer({headless: false,});
myUITestPlayer.run(runConfig);

第四步:运行测试文件

在以后工程下的命令行中执行下述指令,即可期待自动化测试运行

node uiTest/test.js

更好的做法是在以后工程的 package.json 中的 scripts 字段中配置如下命令:

{
  "scripts": {"uiTest": "node uiTest/test.js"}
}

配置完之后,在以后工程下的命令行中执行下述指令即可

npm run uiTest

配置阐明

初始化 options 配置阐明

{
  chromiumPath?: string; // chromiumPath 浏览器文件的寄存目录, 如果须要应用本地的 chromium
  headless?: boolean; // 是否是无浏览器模式,默认 true
  ignoreHTTPSErrors?: boolean; // 是否疏忽 https 的报错,默认 true
  fullScreen?: boolean; // 当 headless 为 false 时,关上的浏览器是否全屏,优先级高于 width 和 height 配置
  width?: number; // 当 headless 为 false 时,关上的浏览器的 width, 默认 800
  height?: number; // 当 headless 为 false 时,关上的浏览器的 height, 默认 600
}

runConfig 配置文件阐明

{
  title?: 'ui 自动化测试' // 生成测试报告的题目
  url: 'http://localhost:7002/', // 测试的页面地址
  screenshotPath?: 'uiTest/result', // 截图寄存的门路,默认会创立 uiTest 目录
  expectedMismatch?: 1000; // 截图比照可承受的像素差,默认 1000
  pageLoadTest?: { // 首页测试
    value?: 'xxxx', // 比对图片的 url, 如果配置了会做截图比对,如果为空只会截图
    trigger?: { // 截图触发机会,默认 time 2000ms
      mode: 'time', // 截图触发的形式,目前有 'time' 和 'dom' 两种形式
      value: 5000 // 数字对应 time,字符串对应 dom
    }
  },
    process: [{ // 测试流程配置
        title?: 'top 视图性能', // 测试性能名称
        step: [ // 测试的具体步骤,如果只须要一次操作则配置单个对象即可,否则按程序配置多个对象
      {
        eventType: 'click', // click: 点击 | hover: 鼠标悬浮 | goTo: 页面跳转 | reload: 刷新页面 | focus: 聚焦页面元素 | keydown: 键盘按键按下 | keyup: 键盘按键抬起 等等浏览器事件
        eventTarget: '.minimap-container',
        eventOption?: {}; // 事件参数
            test?: {
              value?: 'xxxx', // 比对图片 url, 配置了会做截图比对,为空只会截图
              trigger?: { // 截图触发机会,默认 time 2000ms
                  mode: 'time', // 触发的形式,目前有 'time' 和 'dom' 两种形式
                  value: 5000 // 数字对应 time,字符串对应 dom
                },
          skipScreenshot?: false; // 是否跳过截屏,默认 false
        },
      }
    ],
  }]
}

留神点

初始化 options
  1. 默认不开启浏览器运行成果,在控制台会实时输入测试过程中的要害信息,能够通过 headless: true 进行开启
  2. width 和 height 是运行浏览器的尺寸,因而过程中的测试截图也是这个尺寸
runConfig
  1. 最终会调用 jest 主动生成测试报告,title 的配置就是用于最终的测试报告
  2. 预设的比对图片的宽高必须和初始化 options 中的宽高尺寸(默认 800*600)放弃同比例,否则缩放后会有压缩或拉伸,会影响比对后果
  3. expectedMismatch 是测试截图和预设比对截图的比照的像素差,完全一致的状况下最后比照的后果是 0,能够依据理论要求的精确度进行数值调整
  4. process 用于配置整体须要测试的流程,测试运行时会依照数组对象的程序执行,外面的每个对象都是一个独自的测试用例。每个测试用例里用 step 字段配置具体的测试操作,大多数状况下能够采纳单个操作来配置,比方点击某个按钮,期待响应后,主动进行截图,形成了一个 step。然而如果是有多个连贯操作形成的测试操作,比方先跳转到某个页面,再进行点击,则在 step 中就要进行跳转和点击两个先后操作的配置。
  5. 测试后果判断,如果没有配置比对图片,则默认每个测试用例都是通过的。如果配置了比对图片,则只有测试截图和比对图片的差别小于设定的 expectedMismatch 值,才判断测试通过。如果一个测试用例的 step 蕴含多个对象的图片比对,则须要满足所有图片比对符合要求,才判断测试通过。
  6. skipScreenshot 个别用于 step 中间断操作中的不重要步骤的跳过截图操作,比方先跳转到某个页面,再进行点击,跳转到某个页面后到截图不是关注的重点,点击的成果才是重点。这时候就能够在这个跳转的操作对象中配置跳过截图。

方案设计

理解了性能和用法之后,上面具体说说性能中的一些具体设计思路和实现计划

设计思路

ui 自动化测试的是否真的须要,跟具体的前端业务有非常亲密的关联,这一点在文章结尾就说了。依据本人的开发教训来看,如果太过繁琐的测试操作(比方全副手写测试用例),往往会有点鸡肋的感觉,所以 sdk 的设计思路就是做出一款比较简单灵便的测试工具。测试的后果判断能够是全自动化的(配置了比对图片,通过图片比对来给出测试后果),也能够是半自动化的(不配置比对图片,只让 sdk 做默认的测试截图,最初人工查阅这些截图做出测试后果的判断)。理论应用下来之后,一个比拟好的实际是第一次测试半自动化,主动生成测试截图,将合乎预期的图片作为后续测试的比对图片,配置好比对图片后,后续在没有 ui 调整的状况下都进行自动化测试。

外围能力的选型

前端的 ui 自动化测试须要用到浏览器的能力,后期的技术调研次要思考的就是 selenium 和 puppeteer, 比照后的大抵论断是 selenium 的能力更强,puppeteer 的应用更敌对。基于轻量化的定位,最终选取了 puppeteer 作为外围框架。如果要做更强的测试能力(如反对多终端,测试浏览器兼容性)那么可能 selenium 会更适宜。此外,在这个过程中,还思考过另外一种 ui 自动化测试的模式,即录屏记录测试人对页面的操作流程,自动化生成测试的脚本。这种计划其实感觉更好,例如 sahi pro 就肯定水平上反对,然而本人走下来遇到的问题比拟多,偏离了 ” 轻量化 ” 的定位。
至于测试脚本的的技术选型,这个因为 jest 和 mocha 都比拟成熟,之前用得都比拟相熟。所以两者其实都是能够的,最终随机抉择了 jest。

puppeteer

在这里不说得太多,能够去 puppeteer 中武官网 以及很多材料上具体看。要害就在于 puppeteer 提供了一套页面操作相干的 api,可能唤起一个 chrome 浏览器,并且模拟出常见的用户操作,比方鼠标事件、键盘事件、页面跳转等,这就让咱们能够通过代码模拟出用户对页面的常见操作,形成了整个自动化测试的主体流程。此外利用 puppeteer 提供的申请拦挡的能力,sdk 封装后就能做到对页面接口的相干测试。

整体逻辑

初始化

sdk 中封装了一个类,在这个类承受初始化参数并进行参数解决,而后就会初始化一个 puppeteer 的实例,启动一个全局浏览器,并依据参数设定启动的浏览器的尺寸、是否是 h5 的页面。

  private async _init() {this.log('process', '程序初始化中...');
    const browserOptions = this.browserOptionsCheck(this.option);
    this.browser = await Puppeteer.launch(browserOptions);
    this.page = await this.browser.newPage();
    await this.runOnH5(this.option?.h5, IPHONE6);
    this.log('process', '程序初始化完结');
  }

执行测试操作

初始化实现后,当 run 办法被调用,就会进入测试操作,首先也会对传入的 runConfig 进行一系列的参数解决和合并。紧接着就开始执行首页测试的逻辑,操作浏览器跳转到首页,进行首页的截图操作,如果以后工程中不存在寄存截图的目录,在这里也会将目录创立好。

  private async _pageLoadTest() {if (this.isClose()) return;
    this.log('process', '开始运行首页测试...');
    const {value, trigger} = this.playConfig.pageLoadTest;
    await this.pageGoto(this.playConfig.url);
    await this.waitForByTrigger(trigger);
    this.mkdirSync(this.playConfig.screenshotPath);
    this.log('process', '首页截图中...');
    const imgPath = `${this.playConfig.screenshotPath}/screenshot_home_page.png`;
    try {
      await this.page.screenshot({path: imgPath,});
      this.log('success', `${imgPath} 截图胜利!`);
    } catch (error) {this.log('error', `${imgPath} 截图失败 `);
    }
    this.log('process', '首页测试完结');
  }

接下来就会进行 process 中配置的测试操作,其实这个测试的逻辑跟 pageLoadTest 的首页测试基本上雷同。为什么会分成两个呢,是因为思考到刚进入首页是一个比拟非凡的节点,心愿把这个页面进行截图保留下来,不论用户有没有配置 pageLoadTest 都会做这么一个操作。而 process 则是齐全交给了使用者去配置,依据配置的后果进行相应的的测试操作。process 的测试代码跟 pageLoadTest 相比多了一些遍历的操作和依据参数不同调用不同的页面操作 api。

 const process = this.playConfig.process;
    for (let i = 0; i < process.length; i++) {const processItem = process[i];
      for (let j = 0; j < processItem.step.length; j++) {const stepItem = processItem.step[j];
        const {eventType, eventTarget, eventOption, test} = stepItem;

        switch (eventType) {
          case processEventType.selectorclick: {await this.page.click(eventTarget, eventOption);
            break;
          }
          case processEventType.hover: {await this.page.hover(eventTarget);
            break;
          }
           case processEventType.goto: {await this.page.goto(eventTarget, eventOption);
            break;
          }
    ......

     const {trigger, skipScreenshot} = test;
        await this.waitForByTrigger(trigger);
        const imgPath = `${this.playConfig.screenshotPath}/screenshot_${processItem.name}_${j}.png`;
        if (!skipScreenshot) {
          try {
            await this.page.screenshot({path: imgPath,});
            this.log('success', `${imgPath} 截图胜利!`);
          } catch (error) {this.log('error', `${imgPath} 截图失败 `);
          }
    ......

    this.log('process', 'process 运行完结');

jest 进行图片比对

等到测试流程都执行实现后,这时候曾经在相应的目录下生成了页面截图的图片。接下来就进入了最初一步:调用 jest 进行图片比照,生成最终的测试报告。这个过程其实能够分为两个步骤:

  1. 启动 jest 测试脚本
  2. 在测试脚本中进行图片比对
启动脚本

在这一步遇到了很多坑,首先要思考到 jest 匹配测试脚本的门路问题,因为最终这个 sdk 是在业务工程中去应用的,当执行自动化测试时,所在的目录是在业务工程的根目录下,而此时 sdk 是在业务工程的 node_modules 下。因而在 jest 的配置文件中,须要把 rootDir 设置为以后文件目录,否则默认就是被执行时的目录,即业务工程的根目录,那么就找不到 sdk 中的 index.test.js 这个测试脚本了。

  rootDir: path.resolve(__dirname, '.'),

这时候又引发了第二个问题,“__dirname”是在 node 环境下的存在的变量,在 sdk 中是获取不到值的,因而须要通过另一种形式去获取以后文件的目录,即:

import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

解决了以后门路的问题后,接下来就要执行 jest 脚本,并把图片比对须要的配置参数传入脚本中。在这里遇到了第三个问题,就是 index.test.js 匹配不到的问题,试了很多种形式,配合 jest 配置中 rootDir、testMatch 和 testPathIgnorePatterns 字段进行了屡次的测试。依据配置阐明实践上感觉应该曾经能够了,然而后果始终有问题。通过重复尝试,最终去掉了 testMatch(同理 testRegex)字段,并用 –runTestsByPath 指定文件门路,终于拜访到了。。。这一步破费了很多工夫,解决之后泪流满面,至于为什么后面的屡次尝试都失败了,可能得从 jest 源码中找答案了。。。

 await execa(
        'npx',
        ['jest', '--runTestsByPath', __dirname + 'index.test.js', '--config', JSON.stringify(configFinal)],
        {stdio: 'inherit',}
      );

配置中的几个重要参数也提一下:

  1. reporters 字段指定了生成报告的工具包和具体的一些配置,jest-html-reporters 会依据测试后果生成一份简略的测试报告
  2. globals 参数是 jest 提供的能够在测试脚本中全局拜访到的字段配置,把 runConfig 的配置挂载在__DEV__字段下,这样在 index.test.js 下就能取到了

这部分具体的代码如下:

try {
      const jestConfig = {
        preset: 'ts-jest',
        testEnvironment: 'node',
        transform: {'^.+\\.(js|ts|tsx)$': 'ts-jest',
        },
        rootDir: path.resolve(__dirname, '.'),
        testPathIgnorePatterns: ['<rootDir>/node_modules/'],
        reporters: [
          'default',
          [
            'jest-html-reporters',
            {
              pageTitle: this.playConfig.title,
              publicPath: `${reportPath}/uiTestReport`,
              filename: 'UITestReport.html',
              openReport: true,
            },
          ],
        ],
        testTimeout: 1000 * 60 * 10,
      };
      const configFinal = {
        globals: {__DEV__: { ...this.playConfig},
        },
        ...jestConfig,
      };
      this.log('process', '开始截图比对');
      this.closeBrowser();
      await execa(
        'npx',
        ['jest', '--runTestsByPath', __dirname + 'index.test.js', '--config', JSON.stringify(configFinal)],
        {stdio: 'inherit',}
      );
    } catch (error) {this.closeBrowser();
      throw new Error('' + error);
    }
    this.log('done', '全副测试运行完结');
  }
图片比对和断言

接下来就进入 index.test.js 中执行具体的图片比对和测试断言,这部分的逻辑也比拟清晰,首页 pageLoadTest 比对和过程 process 配置比对,如果没有提供比对图片的,则只有截图存在就通过测试用例;如果提供了比对图片的,调用图片像素比对办法,将测试截图和比对图片进行像素比对,满足要求则通过测试用例,不满足则不通过,同时生成比对后的后果图片。至此整个测试流程完结,期待生成测试报告。以首页比对为例,代码如下:

describe(playConfig.globals.__DEV__.title, () => {const { screenshotPath, pageLoadTest, process, expectedMismatch} = playConfig.globals.__DEV__;
  it('首页测试', async () => {
    const originImagePath = pageLoadTest.value;
    const screenShotImagePath = screenshotPath + '/screenshot_home_page.png';
    const diffImagePath = screenshotPath + '/diff_home_page.png';
    if (originImagePath) {const diffRes = await diffImage(originImagePath, screenShotImagePath, diffImagePath, expectedMismatch);
      try {expect(diffRes).toBeTruthy();} catch (error) {throw new Error('首页截图比照超过预期像素差');
      }
    } else {fs.readFile(screenShotImagePath, (err, data) => {
        try {expect(!!data).toBeTruthy();} catch (error) {throw new Error('读取首页截图失败');
        }
      });
    }
  });

测试信息输入

如果启动 sdk 的时候用的是无浏览器模式,那么测试者是没有直观感触测试过程的,测试过程中破费的工夫也不算短,直到整个自动化测试流程跑完之后才会主动跳出测试报告的页面。这对测试者来说是很突兀的,甚至中途的期待过程中可能就认为出错了。因而必要的测试过程中的信息反馈是很重要的,如果仔细观察上述的一些代码,会发现有 this.log 的输入,如:

this.log('process', '程序初始化中...');

这是 sdk 中基于 chalk 这款工具封装的终端信息输入办法,理论中的应用成果如下:

有了这些信息的反馈,给测试者的体验成果就会好很多了

成果展现

最初附上一个理论的测试成果 demo(ps. 录制好视频 demo 才发现居然不反对上传本地视频 … 心愿看到 sf 早日反对吧)

运行测试

测试截图

测试报告

正文完
 0