2年多没更新博客了,似乎进了某团队之后就没什么时间深入专研技术了,现在终于可以挤出些时间总结一下工作中遇到的技术问题了。
单元测试的概念和作用就不多说了,本文主要说说单元测试的打桩。
单元测试的粒度通常是函数级,而大部分业务类代码不可能只有一个函数,因此我们在进行单元测试的时候就要对每个函数进行独立的测试,但是对单一函数进行测试的时候不可避免会调用到其他函数和接口,为了简化测试用例的编写和测试环境的搭建,我们通常“假定”调用的其他函数和外部接口都是“正确”的并且可以根据输入返回“预期”的结果,这就需要 Mock(打桩)方式来对这些外部函数和接口进行处理,来模拟我们希望的处理方式,简化其内部实现。而在单元测试过程中,有时需要统计某个外部接口被调用的次数、每次调用的参数、返回值等信息,这时就需要通过 Proxy(代理)的方式将原接口“打包”一下,做个代理。
JavaScript 的灵活性使得代理和打桩都异常简单:写一个函数,在其中调用被代理函数并记录参数、调用次数等信息,再将这个函数赋给被代理的函数,齐活了;打桩更是简单,直接将桩函数赋给原函数,当然也可以轻松的结合代理函数来处理。一个比较通用的原型如下:
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;
}
使用方法也很简单:
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 方法恢复被代理的函数。
这份代码有问题么?似乎没啥问题。能解决所有问题么?似乎也不能,试想这样一段代码:
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 当作返回值。最终的代理生成函数和相关测试函数为:
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,不知道头晕的同学有没有缓过来,欢迎大家与我交流。