用笨办法应对充满不确定性的未来

微信小程序自动化测试之拦截 API 请求

2023.01.06

TL;DR

封装一个 InspectAPI ,在测试脚本中执行会引起小程序网络请求的操作之前调用,可以获取小程序中网络请求的出参和入参。

原理是综合使用 miniProgram.mockWxMethodminiProgram.exposeFunction 这两个miniprogram-automator自动化库中的 api。

使用方法如下:

let orderNo = '';
await InspectApi({
    miniProgram,
    once: true,
    mockApi: mockApi,
    inspect: (req, res) => {
        let {data} = res.data;
        orderNo = data?.orderNo;
        console.log('传入InspectApi的回调函数中打印入参', orderNo);
    },
})
await page.waitFor(1000 * 3)
console.log(orderNo);

是不是挺符合要求的?

前言

在做自动化测试的时候,会需要对请求 api 进行检查。例如:

  • 需要等待某个请求是否成功
  • 需要获取某个请求的入参和出参

真实场景如购买商品下单时,需要在下单页面拿到调用下单接口返回的订单号,在订单列表页面检查该订单号的数 据。

而在没有做拦截 API 请求时,一种方式是在下单页面代码中将接口返回的订单号显示在页面元素上。

但这样操作的缺点是不够通用,对业务代码的侵入性很强,并且自动化脚本无法感知订单号显示的时机,导致需要 设置比较长的等待时间,而且还不是次次都能成功。

方案设计

方案一

综合使用 miniProgram.mockWxMethodminiProgram.exposeFunction 这两个方法:

  • 使用 miniProgram.exposeFunction 能够在小程序运行时中调用测试脚本中的代码
  • 借助 miniProgram.mockWxMethod 能够使用自定义的函数去覆盖小程序中的 wx 对象上的方法

再加上其他的一些方法如: miniProgram.restoreWxMethod 就可以实现一次性有效的拦截

伪代码如下

await miniProgram.exposeFunction('mockRequest',function(data){
    console.log('data')
})

await miniProgram.mockWxMethod('request',function(obj,options){
    return new Promise((resolve)=>{
        obj.success = (res) => {
            if(matchRule(obj.url)){
                window['mockRequest'](res)
            }
            resolve(res)
        }
        obj.fail = (res) => {resolve(res)}
        this.origin(obj)
    })
}, options)

解释:

  • 先使用 miniProgram.exposeFunction 方法向小程序环境中暴露一个 mockRequest 方法,该方法实际是执行测 试脚本中的函数,将函数入参打印出来。
  • 接着使用 miniProgram.mockWxMethod 方法来覆盖小程序内部的 wx.request 方法,并在执行过程中插入自 定义代码,获取网络请求的出参和入参,通过执行上一步中暴露到小程序中的函数,将小程序中的数据传递到测 试脚本。

最终通过这两个方法的结合,实现了将小程序内部的数据传递到测试脚本中的目的

方案二

在运行脚本时开启一个代理服务,在运行小程序开发者工具的时候设置代理。这样所有的请求都经过我们的代理服 务。

实现思路:

参考京东的 Tiga ,它采用 AnyProxy 来进行二次开发,我们也可以参照执行。

比较

方案一:实现起来比较简单,也能够实现动态拦截。

方案二:完全不侵入代码运行时就可以拦截网络请求,并且能够做到实时的动态拦截。缺点是需要开启代理服务,涉及 到进程间通讯,会比较复杂。

取舍:

方案二的代理方案的,需要使用 AnyProxy 等第三方库来实现代理,增加了外部库的引入,并且需要对开发者工具做额外的配置。

考虑到方案一实现起来较简单,只需要封装一个函数就能实现需求,实现成本比较低,可以先采用该方案快速跑通 流程,验证可行性,后期如果出现性能问题,也可以优化该函数的实现,增加对方案二的支持。

类型设计

所以无论我们用方案一还是方案二来实现,我们可以先设计好我们的 InspectApi 方法的接口。

import MiniProgram from "miniprogram-automator/out/MiniProgram";

interface InspectApiParams {
    miniprogram: MiniProgram;
    once: boolean;
    mockApi: string;
}
interface InspectApi {
    (obj:InspextApiParams): Promise<void>
}

核心代码:

await miniProgram.mockWxMethod('request', function (obj, options) {
    return new Promise((resolve) => {
        obj.success = res => {
            if (obj.url.includes(options.mockApi)) {
                // @ts-ignore
                window[options.mockRequestFuncName](obj, res);
                if (options.once) {
                    // @ts-ignore
                    window[options.cancelApiFuncName]();
                }
            }
            resolve(res);
        };
        obj.fail = res => resolve(res);
        // @ts-ignore
        this.origin(obj);
    });
}, {
    mockApi, once, cancelApiFuncName, mockRequestFuncName
});

最终函数实现

将以上代码缝合到一起,全部放到一个函数中

const InspectApi = async ({ miniProgram, inspect, mockApi, once }) => {
    let mockWxRequest;
    //  返回一个取消mock的函数
    const cancel = async () => {
        await miniProgram.restoreWxMethod('request');
    };
    let cancelApiFuncName = 'cancelMockApi' + Date.now();
    await miniProgram.exposeFunction(cancelApiFuncName, () => {
        cancel();
    });
    const mockRequestFuncName = 'mockWxRequest' + Date.now();
    await miniProgram.exposeFunction(mockRequestFuncName, async (req, res) => {
        inspect(req, res);
    });
    await miniProgram.mockWxMethod('request', function (obj, options) {
        return new Promise((resolve) => {
            obj.success = res => {
                if (obj.url.includes(options.mockApi)) {
                    // @ts-ignore
                    window[options.mockRequestFuncName](obj, res);
                    if (options.once) {
                        // @ts-ignore
                        window[options.cancelApiFuncName]();
                    }
                }
                resolve(res);
            };
            obj.fail = res => resolve(res);
            // @ts-ignore
            this.origin(obj);
        });
    }, {
        mockApi, once, cancelApiFuncName, mockRequestFuncName
    });
    return cancel;
};

用法

let orderNo = '';
await InspectApi({
    miniProgram,
    once: true,
    mockApi: mockApi,
    inspect: (req, res) => {
        let {data} = res.data;
        orderNo = data?.orderNo;
        console.log('传入InspectApi的回调函数中打印入参', orderNo);
    },
})
await page.waitFor(1000 * 3)
console.log(orderNo);

总结

核心难点是在 mockWxMethod 时,传入的函数会被序列化,导致无法使用闭包来引用外部变量。因此需要别的方式

miniprogram.mockWxMethod 有两种入参;第二个参数可以传函数:

miniProgram.mockWxMethod(method: string, result: any): Promise<void>
miniProgram.mockWxMethod(method: string, fn: Function | string, ...args: any[]): Promise<void>

第二个参数是函数时,其他参数会被汇总成对象,作为参数传入 fn 的第二个参数。

于是我们可以这样调用

miniProgram.mockWxMethod('request', function (obj, options) {
    console.log(obj,options)
}, {
    mockApi, once, cancelApiFuncName, mockRequestFuncName
});
  • obj 是小程序中调用 wx.request 时传的参数
  • options 是调用 miniProgram.mockWxMethod 时的第三个参数
微信文档站点中关于 mockWxMethod 的参数说明

微信文档站点中关于 mockWxMethod 的参数说明

微信文档中没有明确说明这个第三个参数的作用,给的例子也没有说明如何使用。还是需要开发者们多加探索。