随着小程序越来越被广泛使用,我们前端的开发工作也从单纯的web开发,扩大到web+小程序的跨端开发。为了提高研发效率,越来越多的web模块需要迁移、更新,兼容小程序以实现跨端复用。而这些模块也会跟随业务进行迭代和版本更新,这时候,我们就需要有良好的测试来保障各端模块的可靠性。
由于我们将许多已有的web模块迁移到小程序,web端的测试相对已经比较完备了。因此我们需要考虑的是:
-
如何快速的将已有的web用例迁移到小程序
-
针对新模块,如何快速编写两端用例。
(我们在web端使用的主要是Puppeteer和Jest的搭配。)
可直接移步最终方案
测试模块类型
我们目前的模块主要是以下三种类型:
- 与环境无关的逻辑层模块
- 与环境关联的逻辑层模块
- 与环境关联的UI组件模块
类型1的模块由于不受环境限制,可与web共用单元测试,无需额外的测试用例开发。
类型3的模块,由于小程序与web端差异较大,比较难实现复用(目前我们的web UI层主要基于React,小程序使用原生开发,同时配合kbone进行部分页面的同构开发)。
我们这里主要针对类型2的模块进行测试用例的迁移。
小程序端测试工具选择
小程序官方目前提供了两种工具来支持小程序测试:
通过官方工具结合Jest, Mocha等测试框架,我们可以实现在小程序环境下的测试。
我们选择了小程序自动化。类似于在Puppeteer运行web端的测试,我们可以通过小程序自动化,操控开发者工具,以实现小程序环境下的测试。两者的相似之处给我们实现测试用例的跨端迁移甚至复用提供了可能性。
如何迁移测试用例
以迁移一个上报模块的测试用例为例,如下是我们已有的一个web上报模块测试用例。
web测试用例
该用例覆盖的路径为:调用imlog.default.error()方法 -> 浏览器将发起请求 -> 对请求参数进行校验
。
test('.error()调用正常', async done => {
const opts = {
project: 'imlogtest',
};
// 检查上报请求的参数
const expector = req => {
try {
const url = req.url();
const method = req.method();
const headers = req.headers();
const body = req.postData();
const data = JSON.parse(body);
expect(url).toBe(DEFAULT_URL); // 请求的url符合预期
expect(method).toBe('POST'); // 请求的method符合预期
expect(headers['content-type']).toBe('text/plain'); // 请求的contentType符合预期
expect(data.url).toBe(TEST_PAGE_URL); // 请求体的url字段符合预期
done();
} catch(error) {
done(error);
}
};
// 监听上报请求并校验参数
page.once('request', expector);
// 在浏览器中执行上报
page.evaluate(
(o) => {
const reportor = window.imlog.default;
reportor.config(o);
reportor.error('test'); // 进行上报
},
opts
);
});
以上主要用到了Puppeteer的Page API。
- page.evaluate 控制页面执行一段逻辑(执行imlog.default.error())
- page.once('request', expector) 监听页面的请求并获取参数(检测是否发起请求并校验请求参数)
小程序用例的设想
小程序自动化给我们提供了一些类似于puppeteer的API:
- miniProgram.evaluate 控制小程序往 AppService 注入代码片段并返回执行结果
如果小程序能够像Puppeteer,使用监听事件的形式拦截到wx API的调用参数,测试用例编写将会方便许多。我们想象的小程序用例将会是如下形式:
test('.error()调用正常', async done => {
const opts = {
project: 'imlogtest',
};
// 检查上报请求的参数
const expector = req => {
try {
// diff:按照特定格式解析出小程序请求参数
const {url, method, headers, body, data} = parseWxReqParams(req);
expect(url).toBe(DEFAULT_URL); // 请求的url符合预期
expect(method).toBe('POST'); // 请求的method符合预期
expect(headers['content-type']).toBe('text/plain'); // 请求的contentType符合预期
expect(data.url).toBe(TEST_PAGE_URL); // 请求体的url字段符合预期
done();
} catch(error) {
done(error);
}
};
// 监听上报请求并校验参数
// todo: miniProgram对象支持once/on等事件方法
miniProgram.once('request', expector);
// 在小程序中执行上报
miniProgram.evaluate(
(o) => {
// diff: 请求方法挂在小程序app对象上
const reportor = getApp().imlog.default;
reportor.config(o);
reportor.error('test'); // 进行上报
},
opts
);
});
只要我们寻找一种方式,通过事件的形式,以miniProgram.on('api', fn)的方式监听到调用API时传递的参数。
在这种形式下,web和小程序用例的差异仅在于:
- web中page对象的操作转移到小程序miniProgram对象完成
- web中挂载在window下的方法,小程序中挂载在app下
- web和小程序解析请求参数的方式不同
wx API拦截方法
首先观察miniProgram对象,通过miniprogram-automator暴露出来的MiniProgram.d.ts,可以发现MiniProgram类本身就继承自EventEmitter。
import { EventEmitter } from 'events';
export default class MiniProgram extends EventEmitter {
// ...
}
接下来就需要寻找方法,在api调用时触发miniProgram对象的emit方法。
有两个自动化API可以帮助我们实现这点。
-
miniProgram.mockWxMethod 允许我们覆盖 wx 对象上指定方法的调用结果。
-
miniProgram.exposeFunction 在 AppService 全局暴露方法,供小程序侧调用测试脚本中的方法。
此时,一个大胆的想法涌上心头💡
我们通过exposeFunction
将miniProgram.emit暴露给小程序AppService,并通过mockWxMethod
,在wx API被调用时emit出入参即可!
最终方案
通过exposeFunction和mockWxMethod得以实现wx API的拦截,以miniProgram.on('api', fn)的方式监听到调用API时的传参,使小程序与web的测试用例编写趋于一致。
import automator from 'miniprogram-automator';
/**
* 预处理逻辑
* 对wx API进行拦截并emit对应事件
* 可以通过miniProgram.on('request', (data) => {})监听到
* */
async function mockMiniProgramMethods(miniProgram) {
// 向小程序运行时暴露'emit'方法,实际调用miniProgram.emit()
await miniProgram.exposeFunction('emit', (evt, data) => {
miniProgram.emit(evt, data);
});
await miniProgram.restoreWxMethod('request');
await miniProgram.mockWxMethod('request', (obj) => {
// 在wx.request API调用时,emit出request事件及其参数
emit('request', obj);
this.origin(obj);
return obj;
});
return miniProgram;
}
主要的测试用例代码如下:
let miniProgram;
beforeAll(async () => {
// 调用mockMiniProgramMethods 注入emit事件,并对wx API进行拦截
miniProgram = await mockMiniProgramMethods(await automator.launch({
projectPath: 'path/to/project', // 项目文件地址
}));
}, LAUNCH_TIMEOUT_WX);
test('.error()调用正常', async done => {
const opts = {
project: 'imlogtest',
};
// 检查上报请求的参数
const expector = req => {
try {
// 从req按照特定格式解析出小程序请求参数
const {url, method, headers, body, data} = parseWxReqParams(req);
done();
} catch(error) {
done(error);
}
};
// Feature: 在wx.request被调用时触发一次expector,并传入wx.request的参数
miniProgram.once('request', expector);
// 在小程序中执行上报
miniProgram.evaluate(
(o) => {
// 请求方法挂在小程序app对象上
const reportor = getApp().imlog.default;
reportor.config(o);
reportor.error('test'); // 进行上报
},
opts
);
});