JavaScript 单元测试中 Proxy(代理)和 Mock(打桩)组件的实现

2年多没更新博客了,似乎进了某团队之后就没什么时间深入专研技术了,现在终于可以挤出些时间总结一下工作中遇到的技术问题了。

单元测试的概念和作用就不多说了,本文主要说说单元测试的打桩。

单元测试的粒度通常是函数级,而大部分业务类代码不可能只有一个函数,因此我们在进行单元测试的时候就要对每个函数进行独立的测试,但是对单一函数进行测试的时候不可避免会调用到其他函数和接口,为了简化测试用例的编写和测试环境的搭建,我们通常“假定”调用的其他函数和外部接口都是“正确”的并且可以根据输入返回“预期”的结果,这就需要 Mock(打桩)方式来对这些外部函数和接口进行处理,来模拟我们希望的处理方式,简化其内部实现。而在单元测试过程中,有时需要统计某个外部接口被调用的次数、每次调用的参数、返回值等信息,这时就需要通过 Proxy(代理)的方式将原接口“打包”一下,做个代理。

JavaScript 的灵活性使得代理和打桩都异常简单:写一个函数,在其中调用被代理函数并记录参数、调用次数等信息,再将这个函数赋给被代理的函数,齐活了;打桩更是简单,直接将桩函数赋给原函数,当然也可以轻松的结合代理函数来处理。一个比较通用的原型如下:

function bind(fn) {
    
var newFn = function () {
        
var args = arguments;
        
var index = newFn.__ProxyMockData;
        newFn.__ProxyMockData.count
++;
        
var info = {
            caller : newFn.caller,
            args : args,
            time : 
-1
        };
        newFn.__ProxyMockData.info.push(info);
        
var time = (new Date()).getTime();
        
var res = newFn.__ProxyMockData.ori.apply(this, args);
        info.time 
= (new Date()).getTime() - time;
        info.res 
= res;
        
return res;
    };
    newFn.__ProxyMockData 
= {
        ori : fn,
        count : 
0,
        info : []
    };
    
return newFn;
}

 使用方法也很简单:

function test() {
    alert(
'test');
}
test(); 
// alert: test

test 
= bind(test);
test(); 
// alert: test

// 打桩
test.__ProxyMockData.ori = function () {
    alert(
'mock');
};
test(); 
// alert: mock

alert(
'test 函数调用次数: ' + test.__ProxyMockData.count); // alert: 2, bind 之前的调用不计算

当然,作为完整的组件,我们方然要把打桩、获取调用信息的过程进行一次封装而避免外部直接使用 __ProxyMockData,而且在打桩前也要记录下原函数,避免原函数丢失无法恢复,同时也可以提供 unbind 方法恢复被代理的函数。

这份代码有问题么?似乎没啥问题。能解决所有问题么?似乎也不能,试想这样一段代码:

function test(msg) {
    
this.msg = msg;
}
test.prototype.test 
= function () {
    alert(
'test: ' + this.msg);
};
var t1 = new test('t1');
t1.test();

test 
= bind(test);
var t2 = new test('t2');
t2.test(); 
// 报错了!

// 打桩
test.__ProxyMockData.ori = function (msg) {
    
this.msg = 'mock: ' + msg;
};
var t3 = new test('t3');
t3.test(); 
// 报错了!

两处报错,原因皆为“ Uncaught TypeError: Object [object Object] has no method 'test' ”,原来,我们创建了代理函数(newFn)时并没有把原函数(fn)的 prototype 原型透传,导致 newFn 的实例并没有 test() 原型。找到了问题的根源就好解决问题了,在创建代理函数中,把原函数中的原型进行透传即可,使用 for in 即可轻松实现,而且 for in 会自动屏蔽 constructor 这个特殊的构造函数(当然以防万一也可以显式做个屏蔽)。另外,在透传的时候也可以再次调用 bind 方法,将原型中的成员函数做个代理再传给代理函数。OK,相信看到这你应该已经晕了,一会直接看代码慢慢理解吧。

当然,对代理函数我们要使用 new 来实例化,而在代理函数的过程中,我们却使用了 newFn.__ProxyMockData.ori.apply 这种相当于直接调用函数的方式,没关系,我们已经把 this 透传到原函数,原函数可以正常的对 this 进行必要的处理,而且构造函数返回 undefined(相当于没有返回)也是没有问题的,因为 JavaScript 引擎会自动判断,如果返回的值不是当前类的实例则直接使用 this 当作返回值。最终的代理生成函数和相关测试函数为:

function bind(fn) {
    
var newFn = function () {
        
var args = arguments;
        
var index = newFn.__ProxyMockData;
        newFn.__ProxyMockData.count
++;
        
var info = {
            caller : newFn.caller,
            args : args,
            time : 
-1
        };
        newFn.__ProxyMockData.info.push(info);
        
var time = (new Date()).getTime();
        
var res = newFn.__ProxyMockData.ori.apply(this, args);
        info.time 
= (new Date()).getTime() - time;
        info.res 
= res;
        
return res;
    };
    newFn.__ProxyMockData 
= {
        ori : fn,
        count : 
0,
        info : []
    };
    
for (var key in fn.prototype) {
        newFn.prototype[key] 
= bind(fn.prototype[key]);
    }
    
return newFn;
}

function test(msg) {
    
this.msg = msg;
}
test.prototype.test 
= function () {
    alert(
'test: ' + this.msg);
};
var t1 = new test('t1');
t1.test(); 
// test: t1

test 
= bind(test);
var t2 = new test('t2');
t2.test(); 
// test: t2

// 打桩
test.__ProxyMockData.ori = function (msg) {
    
this.msg = 'mock: ' + msg;
};
var t3 = new test('t3');
t3.test(); 
// test: mock: t3

// 打桩
test.prototype.test.__ProxyMockData.ori = function (msg) {
    alert(
'mock: ' + this.msg);
};
var t4 = new test('t4');
t4.test(); 
// mock: mock: t4

 

OK,不知道头晕的同学有没有缓过来,欢迎大家与我交流。

posted @ 2010-12-05 01:00  田嵩  阅读(2772)  评论(2编辑  收藏  举报