眾所周知,Angular所用的單元測試框架是Karma+Jasmine柱宦,最近在寫Angular的Unit Test的時(shí)候凄硼,在Given“創(chuàng)建測試條件”部分會在很多地方用到Spy去模擬和監(jiān)測函數(shù)調(diào)用,而jasmine為我們提供的關(guān)于Spy的函數(shù)有很多種捷沸,比如createSpyObj,createSpy狐史,SpyOn等等痒给,而這些方法命名相似但是用法卻不相同,常常讓人容易混淆而產(chǎn)生很多錯誤骏全,下面就通過研讀Jasmine關(guān)于Spy的源碼來弄清楚這些Spy函數(shù)到底是干什么的苍柏,在什么場合下使用它們。
先從createSpyObj開始研究:
j$.createSpyObj?=?function(baseName,?methodNames)?{
var?baseNameIsCollection?=?j$.isObject_(baseName)?||?j$.isArray_(baseName);
if?(baseNameIsCollection?&&?j$.util.isUndefined(methodNames))?{
methodNames?=?baseName;
baseName?=?'unknown';
}
var?obj?=?{};
var?spiesWereSet?=?false;
if?(j$.isArray_(methodNames))?{
for?(var?i?=?0;?i?<?methodNames.length;?i++)?{
obj[methodNames[i]]?=?j$.createSpy(baseName?+?'.'?+?methodNames[i]);
spiesWereSet?=?true;
//如果參數(shù)2是method的數(shù)組姜贡,則調(diào)用createSpy(base.method)
}
}else?if?(j$.isObject_(methodNames))?{
for?(var?key?in?methodNames)?{
if?(methodNames.hasOwnProperty(key))?{
obj[key]?=?j$.createSpy(baseName?+?'.'?+?key);
obj[key].and.returnValue(methodNames[key]);
spiesWereSet?=?true;
//如果參數(shù)2是method:returnValue的鍵值對組成的對象试吁,則除了調(diào)用createSpy(base.method),還用“and.returnValue”來定義了方法的返回值
}
}
}
if?(!spiesWereSet)?{
throw?'createSpyObj?requires?a?non-empty?array?or?object?of?method?names?to?create?spies?for';
}
return?obj;
};
再來看SpyOn:
this.spyOn?=?function(obj,?methodName)?{
//開始是一連串的錯誤處理楼咳,這些錯誤是在寫UT的時(shí)候經(jīng)常出現(xiàn)的錯誤熄捍,可以對號入座
if?(j$.util.isUndefined(obj)?||?obj?===?null)?{
throw?new?Error(getErrorMsg('could?not?find?an?object?to?spy?upon?for?'?+?methodName?+?'()'));
}
if?(j$.util.isUndefined(methodName)?||?methodName?===?null)?{
throw?new?Error(getErrorMsg('No?method?name?supplied'));
}
if?(j$.util.isUndefined(obj[methodName]))?{
throw?new?Error(getErrorMsg(methodName?+?'()?method?does?not?exist'));
}
if?(obj[methodName]?&&?j$.isSpy(obj[methodName])??)?{
if?(?!!this.respy?){
return?obj[methodName];
}else?{
throw?new?Error(getErrorMsg(methodName?+?'?has?already?been?spied?upon'));
}
}
var?descriptor;
try?{
descriptor?=?Object.getOwnPropertyDescriptor(obj,?methodName);
}?catch(e)?{
//?IE?8?doesn't?support?`definePropery`?on?non-DOM?nodes
}
if?(descriptor?&&?!(descriptor.writable?||?descriptor.set))?{
throw?new?Error(getErrorMsg(methodName?+?'?is?not?declared?writable?or?has?no?setter'));
}
var?originalMethod?=?obj[methodName],
spiedMethod?=?j$.createSpy(methodName,?originalMethod),
//這里調(diào)用了createSpy,createSpy的param1是這個(gè)Spy的名字母怜,意義不大余耽;param2是要去Spy的函數(shù)
restoreStrategy;
if?(Object.prototype.hasOwnProperty.call(obj,?methodName))?{
restoreStrategy?=?function()?{
obj[methodName]?=?originalMethod;
};
}?else?{
restoreStrategy?=?function()?{
if?(!delete?obj[methodName])?{
obj[methodName]?=?originalMethod;
}
};
}
currentSpies().push({
restoreObjectToOriginalState:?restoreStrategy
});
obj[methodName]?=?spiedMethod;
return?spiedMethod;
};
再來看一下createSpyObj和spyOn共同用到的方法createSpy(),也可以單獨(dú)調(diào)用
j$.createSpy?=?function(name,?originalFn)?{
return?j$.Spy(name,?originalFn);
};
很簡單苹熏,就是調(diào)用了j$.Spy這個(gè)方法碟贾,
繼續(xù)看最底層的Spy():
getJasmineRequireObj().Spy?=?function?(j$)?{
var?nextOrder?=?(function()?{
var?order?=?0;
return?function()?{
return?order++;
};
})();
/**
*?_Note:_?Do?not?construct?this?directly,?use?{@link?spyOn},?{@link?spyOnProperty},?{@link?jasmine.createSpy},?or?{@link?jasmine.createSpyObj}
*?@constructor
*?@name?Spy
*/
function?Spy(name,?originalFn)?{
var?numArgs?=?(typeof?originalFn?===?'function'???originalFn.length?:?0),
wrapper?=?makeFunc(numArgs,?function?()?{
//做了一個(gè)包裝函數(shù),作為虛擬調(diào)用
return?spy.apply(this,?Array.prototype.slice.call(arguments));
}),
spyStrategy?=?new?j$.SpyStrategy({
//Spy策略:處理Spy的and屬性:callThrough執(zhí)行調(diào)用,?returnValue指定返回值,?callFake執(zhí)行指定函數(shù)轨域,throwError拋出異常袱耽,stub原始狀態(tài)
name:?name,
fn:?originalFn,
getSpy:?function?()?{
return?wrapper;
}
}),
callTracker?=?new?j$.CallTracker(),
//Spy追蹤:any,count干发,argsFor(index),allArgs,?all(調(diào)用的上下文和參數(shù)),?mostRecent朱巨,first,reset
spy?=?function?()?{
/**
*?@name?Spy.callData
*?@property?{object}?object?-?`this`?context?for?the?invocation.
*?@property?{number}?invocationOrder?-?Order?of?the?invocation.
*?@property?{Array}?args?-?The?arguments?passed?for?this?invocation.
*/
var?callData?=?{
object:?this,
invocationOrder:?nextOrder(),
args:?Array.prototype.slice.apply(arguments)
};
callTracker.track(callData);
var?returnValue?=?spyStrategy.exec.apply(this,?arguments);
callData.returnValue?=?returnValue;
return?returnValue;
};
function?makeFunc(length,?fn)?{
switch?(length)?{
case?1?:?return?function?(a)?{?return?fn.apply(this,?arguments);?};
case?2?:?return?function?(a,b)?{?return?fn.apply(this,?arguments);?};
case?3?:?return?function?(a,b,c)?{?return?fn.apply(this,?arguments);?};
case?4?:?return?function?(a,b,c,d)?{?return?fn.apply(this,?arguments);?};
case?5?:?return?function?(a,b,c,d,e)?{?return?fn.apply(this,?arguments);?};
case?6?:?return?function?(a,b,c,d,e,f)?{?return?fn.apply(this,?arguments);?};
case?7?:?return?function?(a,b,c,d,e,f,g)?{?return?fn.apply(this,?arguments);?};
case?8?:?return?function?(a,b,c,d,e,f,g,h)?{?return?fn.apply(this,?arguments);?};
case?9?:?return?function?(a,b,c,d,e,f,g,h,i)?{?return?fn.apply(this,?arguments);?};
default?:?return?function?()?{?return?fn.apply(this,?arguments);?};
}
}
for?(var?prop?in?originalFn)?{
if?(prop?===?'and'?||?prop?===?'calls')?{
throw?new?Error('Jasmine?spies?would?overwrite?the?\'and\'?and?\'calls\'?properties?on?the?object?being?spied?upon');
}
wrapper[prop]?=?originalFn[prop];
}
wrapper.and?=?spyStrategy;
wrapper.calls?=?callTracker;
return?wrapper;
}
return?Spy;
};
由此可以得到铐然,createSpyObj蔬崩、createSpy恶座、SpyOn、Spy這幾個(gè)方法的調(diào)用關(guān)系:
它們適用的場合如圖所示:
解釋:
createSpyObj:原本沒有對象沥阳,無中生有地去創(chuàng)建一個(gè)對象跨琳,并且在對象上創(chuàng)建方法,然后去spy上面的方法
spyOn:原本有對象桐罕,對象上也有方法脉让,只是純粹地在方法上加個(gè)spy
createSpy:原本有對象,但是沒有相應(yīng)的方法功炮,虛擬地創(chuàng)建一個(gè)方法(虛線)溅潜,在虛擬的方法上去spy。如果對象上原來有方法薪伏,也可以用createSpy去spy滚澜,也就是無論有沒有這個(gè)方法,createSpy都會去spy你指定的方法嫁怀。
常見的出錯信息:
基本上出錯的信息都是在spyOn函數(shù)上设捐,摘錄出來以備查找原因:
'could?not?find?an?object?to?spy?upon?for?'?+?methodName?+?'()'
spy的對象為null或undefined
'No?method?name?supplied’
spy的方法為null或undefined
methodName?+?'()?method?does?not?exist'
spy的方法不存在對象上(spyOn必須要在存在的方法上去spy)
·methodName?+?'?has?already?been?spied?upon'
已經(jīng)有一個(gè)spy在這個(gè)方法上了,看看有沒有地方已經(jīng)spy了它