介紹
使用測試驅動開發大半年了,我還是對Stub和Mock的認識比較模糊,沒有進行系統整理。
今天查閱了相關資料,覺得寫得很不錯,所以我試圖在博文中對資料進行整理一下,再加上一些自己的觀點。
本文是目前我對Stub和Mock的認識,難免有偏差,歡迎大家拍磚。
分析
Stub和Mock都是屬於測試替身,對類型細分的話可以分為:
- Dummy Object
- Fake Object
- Test Stub
- Test Spy
- Mock Object
前四項屬於Stub,最后的Mock Object屬於Mock。
類型分析
Dummy Object(啞對象)
測試代碼僅僅是需要使用它來通過編譯,實際上用不到它。如測試A類的run方法,需要在創建A類的實例時需要傳入B類實例,但run方法並沒有用到B類實例。在測試時需要傳入B類的啞對象new NullB()(如“new A(new NullB())”),讓其通過編譯。這里的NullB是一個空類,沒有具體實現。
Fake Object(假對象)
假對象相對於啞對象來說,要對耦合的組件有一些簡單的實現,實現我們在測試中要用到的方法,指定期望的行為(如返回期望的值)。假對象適用於替換產品代碼中使用的全局對象,或者創建的類。這里注意的是要先對被替換的全局對象或類進行備份,然后在測試完成后進行恢復。
示例1(替換全局對象):
//產品代碼 function A(){ this.num = 0; } A.prototype.run = function(){ this.num = window.b.getNum(); }; //測試代碼 describe("測試A類的run方法", function(){ var temp = null; function backUp(){ window.b = window.b || {}; temp = YYC.Tool.extendDeep(window.b); } function restore(){ window.b = temp; } beforeEach(function(){ backUp(); }); afterEach(function(){ restore(); }); it("獲得數字", function () { window.b = { //假對象 getNum: function(){ return 1; } } var a = new A(); a.run(); expect(a.num).toEqual(1); }); });
示例2(替換類):
//產品代碼 function A() { this.num = 0; this._b = new B(); } A.prototype.run = function () { this.num = this._b.getNum(); }; //測試代碼 describe("測試A類的run方法", function () { var temp = null; function backUp() { window.B = window.B || function () {}; temp = B; } function restore() { window.B = temp; } beforeEach(function () { backUp(); }); afterEach(function () { restore(); }); it("獲得數字", function () { window.B = function () { }; window.B.prototype.getNum = function () { return 1; }; var a = new A(); a.run(); expect(a.num).toEqual(1); }); });
Test Stub(測試樁)
測試樁與假對象有點類似,也要實現與產品代碼耦合的組件,指定期望的行為。這里最大的不同是測試樁需要注入到產品代碼中,從而在測試產品代碼時替換組件,執行樁的行為。使用測試樁不需要進行備份和還原。
示例:
//產品代碼 function A(b) { this.num = 0; this._b = b; } A.prototype.run = function () { this.num = this._b.getNum(); }; //測試代碼 describe("測試A類的run方法", function () { it("獲得數字", function () { var stub_B = { //B類的樁 getNum: function(){ return 1; } }; var a = new A(stub_B); //注入樁 a.run(); expect(a.num).toEqual(1); }); });
Test Spy(嗅探樁)
與測試樁類似,但是可以記錄樁使用的記錄,並進行驗證。
示例:
可以使用jasmine的spy來舉例。
//產品代碼 function A(b) { this.num = 0; this._b = b; } A.prototype.run = function () { this.num = this._b.getNum(); }; //測試代碼 describe("測試A類的run方法", function () { it("獲得數字", function () { var stub_b = { getNum: function(){ return 1; } }; spyOn(stub_b, "getNum").andCallThrough(); //嗅探樁的getNum方法 var a = new A(stub_b); //注入樁 a.run(); expect(a.num).toEqual(1); expect(stub_b.getNum).toHaveBeenCalled(); //驗證調用過樁的getNum方法 }); });
Mock Object(模擬對象)
設定產品代碼中耦合的類的期望的行為,然后驗證期望的行為是否發生,從而達到測試產品代碼行為的目的。適用於驗證一些void的行為。例如:在某個條件發生時,要記錄Log。這種情景,用stub就很難驗證,因為對目標物件來說,沒有回傳值,也沒有狀態變化,就只能通過mock object來驗證目標物件是否正確的與Log介面進行互動。
示例:
//產品代碼 function A(b) { this.num = 0; this._b = b; } A.prototype.run = function () { this.num = this._b.getNum(2); }; //測試代碼(Mock為偽代碼) describe("測試A類的run方法", function () { it("獲得數字", function () { var mockB = Mock.createMock({ getNum: function(){} }); //如果B類存在的話,也可以直接傳入B的原型:var mockB = Mock.createMock(B.prototype); Mock.expect(mockB.getNum, 2).return(1).times(1); var a = new A(mockB); a.run(); expect(a.num).toEqual(1); Mock.verify(); //驗證期望的行為發生:mockB的getNum傳入的參數為2;調用了1次mockB.getNum }); });
Mock(Mock Object)與Spy(Test Spy)的比較
相同點
- 都要注入到產品代碼中。
不同的
- Mock是替換整個被Mock的類,這個類可以存在也可以不存在。而Spy是使用一個已經存在的類,嗅探其中的部分方法。
- 從流程中來說,Mock是先設定被Mock的類的期望行為,然后驗證期望的行為是否發生。Spy是記錄下樁的方法的使用記錄(如傳入的參數,調用的次數等),然后再對記錄進行驗證。
Mock退化為Stub
//產品代碼 function A(b) { this.num = 0; this._b = b; } A.prototype.run = function () { this.num = this._b.getNum(2); }; //測試代碼(Mock為偽代碼) describe("測試A類的run方法", function () { it("獲得數字", function () { var mockB = Mock.createMock({ getNum: function(){} }); //如果B類存在的話,也可以直接傳入B的原型:var mockB = Mock.createMock(B.prototype); Mock.expect(mockB.getNum).return(1); //只指定返回值,沒有期望的參數或期望調用的次數。因此不用verify來驗證了! var a = new A(mockB); a.run(); expect(a.num).toEqual(1); }); });
也可以用Stub來達到相同的效果:
//產品代碼 function A(b) { this.num = 0; this._b = b; } A.prototype.run = function () { this.num = this._b.getNum(); }; //測試代碼 describe("測試A類的run方法", function () { it("獲得數字", function () { var stub_B = { getNum: function(){ return 1; } }; var a = new A(stub_B); a.run(); expect(a.num).toEqual(1); }); });
總結
在比較簡單的情況下(如需要啞對象來通過編譯,或是需要測試樁來替換耦合的組件),使用Stub。
如果需要驗證耦合組件的行為,可以使用Spy或Mock。
參考資料
《xUnit測試模式--測試碼重構》