解讀Jasmine的Spy機(jī)制

眾所周知,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了它

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末塘淑,一起剝皮案震驚了整個(gè)濱河市萝招,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌存捺,老刑警劉巖槐沼,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異捌治,居然都是意外死亡岗钩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門具滴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凹嘲,“玉大人,你說我怎么就攤上這事构韵≈懿洌” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵疲恢,是天一觀的道長凶朗。 經(jīng)常有香客問我,道長显拳,這世上最難降的妖魔是什么棚愤? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上宛畦,老公的妹妹穿的比我還像新娘瘸洛。我一直安慰自己,他們只是感情好次和,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布反肋。 她就那樣靜靜地躺著,像睡著了一般踏施。 火紅的嫁衣襯著肌膚如雪石蔗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天畅形,我揣著相機(jī)與錄音养距,去河邊找鬼。 笑死日熬,一個(gè)胖子當(dāng)著我的面吹牛棍厌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竖席,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼定铜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了怕敬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤帘皿,失蹤者是張志新(化名)和其女友劉穎东跪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鹰溜,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡虽填,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了曹动。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斋日。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墓陈,靈堂內(nèi)的尸體忽然破棺而出恶守,到底是詐尸還是另有隱情,我是刑警寧澤贡必,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布兔港,位于F島的核電站,受9級特大地震影響仔拟,放射性物質(zhì)發(fā)生泄漏衫樊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望科侈。 院中可真熱鬧载佳,春花似錦、人聲如沸臀栈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挂脑。三九已至藕漱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間崭闲,已是汗流浹背肋联。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留刁俭,地道東北人橄仍。 一個(gè)月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像牍戚,于是被迫代替她去往敵國和親侮繁。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容