我们能用Puppteer做些什么

1,800 阅读8分钟

背景

  • 故事回顾 当有人说你页面有性能问题,该怎么办
  • 上次产品嫌弃我页面加载太慢,我优化了一波,但是在优化中也遇到了不少糟心事
  • 测试: 你怎么天天在测试环境瞎搞,你看看页面好几次都报错加载不出来,你能不能自测呀,怪不得产品说你是优秀的倒闭员工
  • : 卑微前端,天天被喷!

这么说我就又不服了,不行,我的反驳,来看看测试反馈的问题吧:

  1. 多语言异步加载,页面价值完毕,语言包未加载完毕,导致页面出现多语言的key
2. 部分特定请求需要添加请求头
3. 页面部分功能不好使了

前戏

欲解决问题,必先深入问题。我们来打至分析问题出现的原因:

  1. 我们原本的多语言加载和vue的初始化是串行的,多语言加载完毕才初始化vue,所以能保证页面多语言显示的正常。但是为了提升首屏的渲染就将多语言和vue的初始化并行了,我们知道异步的加载一定要比vue的初始化慢,所以不能为了优化而优化,要考虑到实际的场景。
  2. 在优化完毕后并没有进行全方位的覆盖测试,导致协议的请求没有添加特殊的header请求头请求报错。
  3. 也是未进行全方位的覆盖测试,部分功能未特殊处理,报错。
  4. 验证码发送失败,页面没有报错提示。测试很懵逼,不知那里出了问题。

结论

除了第一个是加载的问题,其他的问题可以归结为,我们没有一个很好的自动化测试体系,去覆盖我们页面上所有的功能点,当我们修改了主要的底层逻辑的时候,跑一遍自动化测试,页面报错就记录下了,然后逐一比对排查修复,这样就能规避很多不必要的麻烦。

如何实现

提到 Web 的自动化测试,很多人熟悉的是 Selenium 2.0(Selenium WebDriver), 支持多平台、多语言、多款浏览器(通过各种浏览器的驱动来驱动浏览器),提供了功能丰富的API接口。而随着前端技术的发展,Selenium 2.0 逐渐呈现出环境安装复杂、API 调用不友好、性能不高等缺点。新一代的自动化测试工具 —— Puppeteer ,相较于 Selenium WebDriver 环境安装更简单、性能更好、效率更高、在浏览器执行 Javascript 的 API 更简单,它还提供了网络拦截等功能。

你可以在浏览器中手动完成的大部分事情都可以使用 Puppeteer 完成!你可以从以下几个示例开始:

  • 生成页面的截图和PDF。
  • 抓取SPA并生成预先呈现的内容(即“SSR”)。
  • 从网站抓取你需要的内容。
  • 自动表单提交,UI测试,键盘输入等
  • 创建一个最新的自动化测试环境。使用最新的JavaScript和浏览器功能,直接在最新版本的Chrome中运行测试。
  • 捕获您的网站的时间线跟踪,以帮助诊断性能问题。

Puppeteer 提供了一种启动 Chromium 实例的方法。 当 Puppeteer 连接到一个 Chromium 实例的时候会通过 puppeteer.launchpuppeteer.connect 创建一个 Browser 对象,在通过 Browser 创建一个 Page 实例,导航到一个 Url ,然后保存截图。一个 Browser 实例可以有多个 Page 实例。 下面就是使用 Puppeteer 进行自动化的一个典型示例:

const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'screenshot.png'});
  await browser.close();
});

综上所述,我们选择基于 Puppeteer 来开发工程的自动化测试工具,通过 Puppeteer 提供的一系列 API ,实现访问目标页面、模拟异常场景、生成截图的过程自动化。最后再通过人工比对截图,判断页面降级处理是否符合预期、用户体验是否友好。

实现方案

自动化流程

  1. 模拟用户访问页面操作;
  2. 拦截网络请求,请求报错,生成错误信息截图;
  3. 关键位置截图;
  4. 模拟异常场景,截图看页面是否符合预期;
  5. 修改页面UA,截图看页面是否在不同端符合预期;
  6. mock请求,修改返回数据,截图看页面是否符合预期;

别说了,开打开打

通过 npm init 初始化项目后, 就可以安装 Puppeteer 依赖了: npm i puppeteer :在安装时自动下载最新版本 Chromium。

const puppeteer = require('puppeteer');
(async () => {
      // 创建浏览器实例
      const browser = await puppeteer.launch({
        headless: false
      });
      // 生成一个table页
      const page = await browser.newPage();
      // 访问某个页面
      await page.goto('https://example.com');
      // 生成页面截图
      await page.screenshot({path: 'screenshot.png'});
      // 访问浏览器
      browser.close();
})();

这样我们就完成了一个最简单的测试的流程。

公共数据配置

为了提高测试脚本的可维护性、扩展性,我们将测试用例的信息都配置到 JSON 文件中,这样编写测试脚本的时候,我们只需关注测试流程的实现。 测试用例 JSON 数据配置包括公用数据(global)私有数据

  • 公用数据(global):各测试用例都需要用到的数据,如:模拟访问的目标页面地址、名字、描述、设备类型等。
  • 私有数据: 各测试用例特定的数据,如测试模块信息、API 地址、测试场景、预期结果等数据。
// 返回qq的UA
import { QQ } from 'util'

{
  "global": {
    "url": "https://xxx.com",
    "pageName": "index",
    "pageDesc": "首页",
    "device": "iPhone 7",
    "name": "张三",
    "idNum": "4115021968XXXXXXXXXX",
    "phone": "13412344321",
    "email": "163@qq.com",
    "UA": QQ,
    ....
    // 企业信息等
  },
  "homePageApi": {
    "moduleDesc": "首页主接口",
    "apiBase": "https://xxx/v2/home/",
    // 业务逻辑
    "type": "认证方式",
    // 页面配置
    "config": "认证方式",
  },
  ......
}

拦截接口请求,修改数据截图

async test () => {
  ... // 创建 Page 实例,访问首页
  const browser = await puppeteer.launch({
    headless: false
  });
  // 生成一个table页
  const page = await browser.newPage();
  // 访问某个页面
  await page.goto(global.url);
  // 设置拦截请求
  await page.setRequestInterception(true) 
  // 监听请求事件,当请求发起后页面会触发这个事件
  page.on("request", interceptionEvent)
 
  ...
}

若测试用例需要拦截不同的请求,或是模拟多种场景,则需要设置多个请求监听事件。且一个事件执行结束后,必须要移除事件监听,才能继续下一个事件监听。

添加事件监听:page.on("request", eventFunction)

移除事件监听:page.off("request", eventFunction)

    // 设置拦截请求
    await page.setRequestInterception(true)
    const interceptionPhone = requestInterception("phone")
    // 添加事件 1 监听
    page.on("request", interceptionPhone)
    await page.goto(url)
    await page.screenshot({
      path: './result/phone.png'
    })
    // 移除事件 1 监听 
    page.off("request", interceptionPhone)
    const interceptionEmail = requestInterception("email")
    // 添加事件 2 监听
    page.on("request", interceptionEmail)
    await page.goto(url)
    await page.screenshot({
      path: './result/email.png'
    })
    // 移除事件 2 监听
    page.off("request", interceptionEmail)

模拟异常数据场景,生成 mock 数据。

// 我们能根据不同的type mock任何想mock的数据
function requestInterception (type) {
  let mockData
  switch (type) {
      // 修改返回状态码
    case "phone":
      if(!global.isReally){
          mockData = { 错误的用户数据 }
      }
      break
    case "email": // 修改返回内容类型
      mockData = {
        contentType: setValue
      }
      break
    ......
    default:
      break
  }
  return async req => {
   // 如果是需要拦截的 API,则通过 req.respond(mockData) 修改返回数据,否则 continue 继续请求别的
      return req.respond(mockData) // 修改返回数据
    }
}

模拟接口返回 500:

  const interception500 = requestInterception("500")
  page.on("request", interception500) // 当请求发起后页面会触发这个事件

模拟异常数据:

 const iconInterception = requestInterception("error")
 page.on("request", iconInterception)

生成 mock 数据有两种实现方案,可依据实际情况而定:

  •  直接通过修改接口真实返回的数据生成 mock 数据,需要先获取接口实时返回数据
  •  本地存储一份完整的接口数据,通过修改本地存储数据的方式生成 mock 数据(本文所述案例均基于此方案实现)

若选择第一种方案,则需先拦截接口请求,通过 req.response() 获取接口实时返回数据,根据测试场景修改实时返回数据作为 mock 数据。

在模拟点击刷新按钮之前,需等待按钮渲染完成,再触发按钮点击。(防止刷新页面后,DOM 还未渲染完成的情况下,因找不到 DOM 导致报错)

取消拦截,恢复网络

await page.setRequestInterception(false)

设置不同的UA,截图页面

async test () => {
  ... // 创建 Page 实例,访问首页
  // 设置UA
  await page.setUserAgent(global.UA)
  // 监听请求事件,当请求发起后页面会触发这个事件
  await page.screenshot({
    path: `./result/${global.UA}.png`
  })
  ...
}

模拟用户操作

用户点击某个按钮

await page.click('.main-content .phoneBtn')

聚焦用户输入框,模拟用户输入

const input = await page.$('#code-content .el-form-item__content .el-input__inner')
await input.tap()
await input.type('123456', {
    delay: 200,
})

我的影片我.gif

用户滚动页面

await page.evaluate((top) => { window.scrollTo(0, top) }, top)

page.evaluate(pageFunction, …args):在当前页面实例上下文中执行 JavaScript 代码,就是调用当前页面的js代码

监听页面报错

page.on('error', (e) => { 
  await page.screenshot({
    path: `./result/${e.error}.png`
  })
})

当然 Puppteer提供的功能远不止这些,大家可以在这里找到答案zhaoqize.github.io/puppeteer-a…

插件形式

整个项目的每个页面功能都是独立的,我们可以将页面功能分开单独抽离维护,形成一个插件系统,当某个页面的工修改了,只对应的修改某个插件即可。

// queue函数主要是一些逻辑判断和业务处理齐核心代码就一句话
function runPromiseByQueue(myPromises) {
  myPromises.reduce(
    (previousPromise, nextPromise) => previousPromise.then(() => nextPromise()),
    Promise.resolve()
  );
}
// 每个插件都返回一个promise,
function run(argv) {
  return new Promise((resolve, reject) => {
    puppeteer.launch().then(async browser => {
      ......
    });
  })
 }

queue([
  phone,
  face,
  bank,
])

运行结果

部分页面的运行结果 我的影片我.gif 相应的我们一共生成了3张错误结果页:

这样就能很清楚的知道我们页面有哪些主要的问题,方便排错和修改。

测试的反馈

17251630766711_.pic.jpg hahahahahahahaha~~~~~!!!依旧是优秀的前端bug制造商。