这是我参与「第四届青训营 」笔记创作活动的第17天
编写测试用例
SDK属于一个需要长期维护和更新的独立库,它被使用在很多业务项目中,要求更加稳定,当出现问题的时候,它的更新成本很高。需要经历:更新代码->发布新版本->业务方更新依赖版本,等流程,而如果在这个流程中,假如SDK又改出其它问题,那将会再启上述循环,业务同事肯定会被麻烦死。随着接入监控的系统增多,在迭代过程中改动任何的代码已经让人开始发慌,因为存在很多流程性的关联逻辑,害怕改出问题。在一次代码的重构和优化过程中,决心完善单元测试和流程测试。
4.1 单元测试
单元测试主要是对一些有明显输入输出的通用方法,比如SDK的utils中的常用方法,SDK的参数配置方法等。而对于监控SDK来说,更多的测试代码主要集中在流程测试,对于单元测试这里就不具体说明了。
4.2 流程测试
监控SDK在业务项目中初始化之后,主要是通过加入探针监听业务项目的运行状态而收集信息并进行上传的,它在大部分情况下并不是业务方调用什么就执行什么。比如我们页面初次加载,SDK在合适的时机会执行首次加载相关信息的收集并上传,那我们需要通过测试代码来模拟这个流程,保障上报的数据是预期的。
我们的SDK运行在浏览器环境中,在node环境下是不支持Web相关API的。因此我们需要让我们的测试代码在浏览器中运行,或者提供相关API的支持。下面我们将会介绍两种不同的方式,来支持我们的测试代码正常运行。
4.2.1 提供 Web 环境的方式
假如我们使用mocha或者jest作为测试框架,可以通过mocha自带的mocha.run方法在html中编写和执行我们的测试代码,并在浏览器中打开运行;jest-lite也可以支持让jest运行在浏览器中。
但有时候我们不想让它打开浏览器,希望在终端中就能完成测试代码运行,可以使用无头浏览器,在node中加载浏览器环境,比如phontomjs或者puppeteer。他们提供了相关的工具,比如mocha-phantomjs就能直接在终端中运行html执行测试流程。
基于写好的html测试文件,再使用mocha-phantomjs和phantomjs,以下是package.json的命令配置。
scripts:{
test: mocha-phantomjs -p ./node_modules/.bin/phantomjs /test/unit/index.html
}
phontomjs已经被废弃了,不被推荐使用。推荐puppeteer,相关的功能和类似工具都有支持。
举例说明:
以前有在WebSocket的代码库中使用过这种方式。因为依赖Web Api: WebSocket。需要通过new WebSocket(),来完成测试流程,而node环境下没有此API。于是使用mocha在html中写测试用例,如果希望全程使用终端跑测试,还可以配合使用mocha-phantomjs让测试的html文件可以在终端中执行而不用打开本地的网页运行。
当然其实完全可以直接在浏览器中打开html查看测试运行结果,而且phantomjs相关的依赖包非常大、安装也比较慢。但当时我们使用了持续继承服务 travis,当我们的代码更新到远程仓库以后,travis将会启动多个独立容器并在终端中执行我们的测试文件,如果不使用mocha-phantomjs在终端中跑测试没有办法在travis中成功通过。
4.2.2 Mock Web API 的方式
在这次完善监控SDK测试的过程中,尝试了另一种方式,全程使用Mock的方式。
上面的Web环境运行方式需要提供浏览器或者无头浏览器。但实际我们需要测试的代码并不是Web API,我们只是使用了它们。我们假定它们是稳定的,我们只需要在乎它的输入输出,如果它们内部出bug了,我们也是不能控制的,那是浏览器开发商的事情。因此我要做的事情仅仅是在node环境中模拟相关的Web API。
拿前面说到的WebSocket举例,因为node中不支持WebSocket,我们没有办法new WebSocket。那假如有完全模拟WebSocket的三方node库,我们就可以在node代码中,直接让执行环境支持WebSocket: const WebSocket = require('WebSocket')。这样我们就不需要在浏览器或者无头浏览器环境下运行了。
下面就具体拿我们的监控SDK中的fetch举例,是如何模拟流程测试的,总的来说要支持下面 3 个内容,
- 启动一个 httpserver 服务提供接口服务
- 引入三方库,让 node 支持 fetch
- node 中手动模拟部分 performance API
首先说明一下SDK中fetch的正常流程,当我们的SDK在业务项目中初始化了之后,SDK会重写fetch,于是业务项目中真正使用fetch做业务接口请求的时候,SDK就能通过之前重写的逻辑获取到http请求和响应信息,同时也会通过performance获取到fetch请求的性能信息,并进行上报。我们要写的测试代码,就是验证这个流程能够顺利完成。
(1)http server
因为是验证fetch完整流程,我们需要启动一个httpserver服务,提供接口来接收和响应这次fetch请求。
(2)mock fetch
node环境中支持fetch的话,我们可以直接使用三方库 node-fetch,在执行环境的顶部,我们就可以提前定义fetch。
/** MockFetch.js */
import fetch from 'node-fetch';
window = {};
window.fetch = fetch;
global.fetch = fetch;
(3)mock performance
而performance就比较特殊一点,没有一个三方的库能够支持。对于fetch流程来说,我们如果要模拟performance,只需要模拟我们使用的PerformanceObserver,甚至一些入参和返回我们也可以只模拟我们需要的。下面的代码是PerformanceObserver的使用例子。在SDK中,我们主要也是使用这一段代码。
/** PerformanceObserver 使用实例 */
var observer = new PerformanceObserver(function(list, obj) {
var entries = list.getEntriesByType('resource');
for (var i=0; i < entries.length; i++) {
// Process "resource" events
}
});
observer.observe({entryTypes: ['resource']});
在浏览器内部performance底层会自动去监听资源请求,我们只是通过它提供PerformanceObserver去收集它的数据。本质上来说,主动收集的行为探针在performance内部实现。
下面我们模拟PerformanceObserver一部分功能,来支持我们需要的测试流程。定义window.PerformanceObserver为构造函数,把传入方法参数fn加入到数组中。mockPerformanceEntriesAdd 是我们需要手动调用的方法,当我们发起一次fetch,我们就手动调用一下此方法,把mock数据传入给注册的监听函数,这样就能使PerformanceObserver的实例接收到我们的mock数据,以此来模拟浏览器中performance内部的行为。
/** MockPerformance.js */
let observerCallbacks = [];
//模拟PerformanceObserver对象,添加资源监听队列
window.PerformanceObserver = function (fn) {
this.observe = function () {};
observerCallbacks.push(fn);
};
//手动触发模拟performance资源队列
window.mockPerformanceEntriesAdd = (resource) => {
observerCallbacks.forEach((cb) => {
cb({
getEntriesByType() {
return [resource];
},
});
});
};
通俗点举例来说,十号公司要给打工人银行卡发工资的,打工人的工资银行卡第二天就会被扣房贷。打工人最关心的保障正常扣房贷否则影响征信。本来打工人只需要关注银行是否成功完成扣款,但是打工人最近丢工作了公司不会打款到工资卡,所以只能拿积蓄卡给自己的扣贷银行卡转钱,让后续银行可以扣钱还房贷。公司就是浏览器performance底层,打工人给自己转钱就是mockPerformanceEntriesAdd,把公司发工资到银行卡替换为自己转钱进去,从被动接收变为主动执行。细品,你细品~
mockPerformanceEntriesAdd就是模拟浏览器的主动行为,入参是性能信息,我们可以直接写死(下方mockData)。看看测试代码
/** test/fetch.js */
import 'MockFetch.js';
import 'MockPerformance.js';
import webReportSdk from '../dist/monitorSDK';
//初始化监控 sdk,sdk 内部会重写 fetch
const monitor = webReportSdk({
appId: 'appid_test',
});
//再次重写 fetch,拦截请求并跳过上报
const monitorFetch = window.fetch;
let reportData;
window.fetch = function () {
//sdk 上报的数据我们会做一个 type 标记,避免 SDK 收集它自己发出的请求信息
if (arguments[1] && arguments[1].type === 'report-data') {
//获取请求内容
reportData = JSON.parse(arguments[1].body);
return Promise.resolve();
}
return monitorFetch.apply(this, arguments);
};
const mockData = {
name: 'xxx.com/api/getData',
entryType: 'resource',
startTime: 90427.23999964073,
duration: 272.06500014290214,
initiatorType: 'fetch',
nextHopProtocol: 'h2',
...
}
test('web api: fetch', (done) => {
//GET
const requestAddress = mockData.name;
fetch(requestAddress, {
method: 'GET',
});
//发送请求后,需要模拟浏览器 performace 数据监听
window.mockPerformanceEntriesAdd(mockData);
//需要一定延迟
setTimeout(()=>{
expect(reportData.resourceList[0].name).toEqual(mockData.name);
` //more expect...
done()
},3000)
})```