2014-03-25 38 views
2

代碼toHaveBeenCalled和toHaveBeenCalledWith正確報告茉莉spyOn沒有被測

module lib { 
    class Person { 
     private _dfd: JQueryDeferred<Topic>; 
     private _topic: Topic; 

     constructor(public firstName: string) { 
      this._dfd = jQuery.Deferred(); 
      this._topic = Bus.topic("user:logon"); 
      this._dfd.done(this._topic.publish); 
     } 

     logon() { 
      this._dfd.resolve(this); 
     } 
    } 

    class ApiService { 
     constructor() { 
      Bus.topic("user:logon").subscribe(this.callLogonApi); 
     } 
     callLogonApi(person: Person) { 
      console.log("Person.firstname: " + person.firstName); 
     } 
    } 

    describe("Bus",() => { 
     var person: Person; 
     var apiService: ApiService; 

     beforeEach(() => { 
      person = new Person("Michael"); 
      apiService = new ApiService(); 
      spyOn(apiService, "callLogonApi"); 

       //or this fails as well 
       //spyOn(apiService, "callLogonApi").and.callThrough(); 
      person.logon(); 
     }); 

     it("should create subscription and catch the published event",() => { 
      expect(apiService.callLogonApi).toHaveBeenCalled(); 

       //this fails too 
      //expect(apiService.callLogonApi).toHaveBeenCalledWith(person); 
     }); 
    }); 
} 

的callLogonApi函數被調用和控制檯被寫入如預期,但輸出是:

Expected spy callLogonApi to have been called. 
Error: Expected spy callLogonApi to have been called. 

*這正與ApiService的構造工作改爲:

constructor() { 
     Bus.topic("user:logon").subscribe((data)=> { this.callLogonApi(data); }); 
    } 

*而spyOn需要

 spyOn(apiService, "callLogonApi").and.callThrough(); 

感謝瑞安的偉大的答案!

回答

6

這裏是發生了什麼事情的縮小版。

首先,這裏的spyOn方法的簡化版本:

function spyOn(obj: any, methodName: string) { 
    var prev = obj[methodName]; 
    obj[methodName] = function() { 
     console.log(methodName + ' got called'); 
     prev(); 
    } 
} 

現在讓我們嘗試了這一點,用一個簡單的類:

/** OK **/ 
class Thing1 { 
    sayHello() { 
     console.log('Hello, world'); 
    } 
} 

var x = new Thing1(); 
spyOn(x, 'sayHello'); 
x.sayHello(); // 'sayHello got called' 

可正常工作。對延期版本,這是你的代碼在做什麼:

/** Not OK **/ 
class Thing2 { 
    private helloMethod; 

    constructor() { 
     this.helloMethod = this.sayHello; 
    } 

    deferredHello() { 
     window.setTimeout(this.helloMethod, 10); 
    } 

    sayHello() { 
     console.log('Hello, world'); 
    } 
} 

var y = new Thing2(); 
spyOn(y, 'sayHello'); 
y.deferredHello(); // Why no spy? 

最後,固定版本。我將解釋爲什麼它很快就會被修復:

/** OK now **/ 
class Thing3 { 
    private helloMethod; 

    constructor() { 
     this.helloMethod =() => { this.sayHello(); } 
    } 

    deferredHello() { 
     window.setTimeout(this.helloMethod, 10); 
    } 

    sayHello() { 
     console.log('Hello, world'); 
    } 
} 

var z = new Thing3(); 
spyOn(z, 'sayHello'); 
z.deferredHello(); // Spy works! 

這是怎麼回事?

請注意,spyOn函數接受一個對象,包裝該方法,然後在對象本身上設置屬性,該對象替換了spied函數實例。這是非常重要的,因爲它會改變方法名稱的屬性查找最終發生的位置。

在正常情況(Thing1),我們在x覆蓋一個屬性(使用spyOn),然後調用該方法相同的方法對x。一切正常,因爲我們正在調用與spyOn包裝完全相同的功能。

在延期的情況下(Thing2),y.sayHello在整個代碼中發生變化。當我們首先在構造函數中獲取它時,我們從該類的原型獲得sayHello方法。當我們spyOny.sayHello,包裝函數是一個新的對象,但我們在早些時候執行的引用仍然指向在原型中執行sayHello

在固定的情況下(Thing3),我們使用一個功能更懶洋洋地得到sayHello值,所以當z.sayHello變化(因爲我們窺探那),該deferredHello調用「看到」新方法的對象,現在是在實例對象而不是類的原型。

+0

瑞恩 - 多好的回答!謝謝! – Mike