上一篇 Jasmine入門(上) 介紹了Jasmine以及一些基本的用法,本篇我們繼續研究Jasmine的其他一些特性及其用法(注:本篇中的例子均來自於官方文檔)。
Spy
Spy用來追蹤函數的調用歷史信息(是否被調用、調用參數列表、被請求次數等)。Spy僅存在於定義它的describe和it方法塊中,並且每次在spec執行完之后被銷毀。
示例1:
1 (function(){ 2 describe("A spy", function() { 3 var foo, bar = null; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 } 10 }; 11 12 spyOn(foo, 'setBar'); // 在foo對象上添加spy 13 14 // 此時調用foo對象上的方法,均為模擬調用,因此不會執行實際的代碼 15 foo.setBar(123); // 調用foo的setBar方法 16 foo.setBar(456, 'another param'); 17 }); 18 19 it("tracks that the spy was called", function() { 20 expect(foo.setBar).toHaveBeenCalled(); //判斷foo的setBar是否被調用 21 }); 22 23 it("tracks all the arguments of its calls", function() { 24 expect(foo.setBar).toHaveBeenCalledWith(123); //判斷被調用時的參數 25 expect(foo.setBar).toHaveBeenCalledWith(456, 'another param'); 26 }); 27 28 it("stops all execution on a function", function() { 29 expect(bar).toBeNull(); // 由於是模擬調用,因此bar值並沒有改變 30 }); 31 }); 32 })();
從示例1中看到,當在一個對象上使用spyOn方法后即可模擬調用對象上的函數,此時對所有函數的調用是不會執行實際代碼的。示例1中包含了兩個Spy常用的expect:
toHaveBeenCalled: 函數是否被調用
toHaveBeenCalledWith: 調用函數時的參數
and.callThrough()
那如果說我們想在使用Spy的同時也希望執行實際的代碼呢?
示例2:
1 (function(){ 2 describe("A spy, when configured to call through", function() { 3 var foo, bar, fetchedBar; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 }, 10 getBar: function() { 11 return bar; 12 } 13 }; 14 15 spyOn(foo, 'getBar').and.callThrough(); // 與示例1中不同之處在於使用了callThrough,這將時所有的函數調用為真實的執行 16 //spyOn(foo, 'getBar'); // 可以使用示例1中的模擬方式,看看測試集執行的結果 17 18 foo.setBar(123); 19 fetchedBar = foo.getBar(); 20 }); 21 22 it("tracks that the spy was called", function() { 23 expect(foo.getBar).toHaveBeenCalled(); 24 }); 25 26 it("should not effect other functions", function() { 27 expect(bar).toEqual(123); // 由於是真實調用,因此bar有了真實的值 28 }); 29 30 it("when called returns the requested value", function() { 31 expect(fetchedBar).toEqual(123); // 由於是真實調用,fetchedBar也有了真實的值 32 }); 33 }); 34 })();
通過在使用spyOn后面增加了鏈式調用and.CallThrough(),這將告訴Jasmine我們除了要完成對函數調用的跟蹤,同時也需要執行實際的代碼。
and.returnValue()
由於Spy是模擬函數的調用,因此我們也可以強制指定函數的返回值。
示例3:
1 (function(){ 2 describe("A spy, when configured to fake a return value", function() { 3 var foo, bar, fetchedBar; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 }, 10 getBar: function() { 11 return bar; 12 } 13 }; 14 15 spyOn(foo, "getBar").and.returnValue(745); // 這將指定getBar方法返回值為745 16 17 foo.setBar(123); 18 fetchedBar = foo.getBar(); 19 }); 20 21 it("tracks that the spy was called", function() { 22 expect(foo.getBar).toHaveBeenCalled(); 23 }); 24 25 it("should not effect other functions", function() { 26 expect(bar).toEqual(123); 27 }); 28 29 it("when called returns the requested value", function() { 30 expect(fetchedBar).toEqual(745); 31 }); 32 }); 33 })();
如果被調用的函數是通過從其他函數獲取某些值,我們通過使用returnValue模擬函數的返回值。這樣做的好處是可以有效的隔離依賴,使測試流程變得更簡單。
and.callFake()
與returnValue相似,callFake則更進一步,直接通過指定一個假的自定義函數來執行。這種方式比returnValue更靈活,我們可以任意捏造一個函數來達到我們的測試要求。
示例4:
1 (function(){ 2 describe("A spy, when configured with an alternate implementation", function() { 3 var foo, bar, fetchedBar; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 }, 10 getBar: function() { 11 return bar; 12 } 13 }; 14 15 spyOn(foo, "getBar").and.callFake(function() { 16 return 1001; 17 }); 18 19 foo.setBar(123); 20 fetchedBar = foo.getBar(); 21 }); 22 23 it("tracks that the spy was called", function() { 24 expect(foo.getBar).toHaveBeenCalled(); 25 }); 26 27 it("should not effect other functions", function() { 28 expect(bar).toEqual(123); 29 }); 30 31 it("when called returns the requested value", function() { 32 expect(fetchedBar).toEqual(1001); 33 }); 34 }); 35 })();
and.throwError()
throwError便於我們模擬異常的拋出。
1 (function(){ 2 describe("A spy, when configured to throw an error", function() { 3 var foo, bar; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 } 10 }; 11 12 spyOn(foo, "setBar").and.throwError("quux"); 13 }); 14 15 it("throws the value", function() { 16 expect(function() { 17 foo.setBar(123) 18 }).toThrowError("quux"); 19 }); 20 }); 21 })();
and.stub
示例5:
1 (function(){ 2 describe("A spy", function() { 3 var foo, bar = null; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 }, 10 getBar: function(){ 11 return bar; 12 } 13 }; 14 15 spyOn(foo, 'setBar').and.callThrough(); // 標記1 16 spyOn(foo, 'getBar').and.returnValue(999); // 標記2 17 }); 18 19 it("can call through and then stub in the same spec", function() { 20 foo.setBar(123); 21 expect(bar).toEqual(123); 22 23 var getValue = foo.getBar(); 24 expect(getValue).toEqual(999); 25 26 foo.setBar.and.stub(); // 相當於'標記1'中的代碼變為了spyOn(foo, 'setBar') 27 foo.getBar.and.stub(); // 相當於'標記2'中的代碼變為了spyOn(foo, 'getBar') 28 bar = null; 29 30 foo.setBar(123); 31 expect(bar).toBe(null); 32 expect(foo.setBar).toHaveBeenCalled(); // 函數調用追蹤並沒有被重置 33 34 getValue = foo.getBar(); 35 expect(getValue).toEqual(undefined); 36 expect(foo.getBar).toHaveBeenCalled(); // 函數調用追蹤並沒有被重置 37 }); 38 }); 39 })();
其他追蹤屬性:
calls:對於被Spy的函數的調用,都可以在calls屬性中跟蹤。
- .calls.any(): 被Spy的函數一旦被調用過,則返回true,否則為false;
- .calls.count(): 返回被Spy的函數的被調用次數;
- .calls.argsFor(index): 返回被Spy的函數的調用參數,以index來指定參數;
- .calls.allArgs():返回被Spy的函數的所有調用參數;
- .calls.all(): 返回calls的上下文,這將返回當前calls的整個實例數據;
- .calls.mostRecent(): 返回calls中追蹤的最近一次的請求數據;
- .calls.first(): 返回calls中追蹤的第一次請求的數據;
- .object: 當調用all(),mostRecent(),first()方法時,返回對象的object屬性返回的是當前上下文對象;
- .calls.reset(): 重置Spy的所有追蹤數據;
示例6:
1 (function(){ 2 describe("A spy", function() { 3 var foo, bar = null; 4 5 beforeEach(function() { 6 foo = { 7 setBar: function(value) { 8 bar = value; 9 } 10 }; 11 12 spyOn(foo, 'setBar'); 13 }); 14 15 it("tracks if it was called at all", function() { 16 expect(foo.setBar.calls.any()).toEqual(false); 17 18 foo.setBar(); 19 20 expect(foo.setBar.calls.any()).toEqual(true); 21 }); 22 23 it("tracks the number of times it was called", function() { 24 expect(foo.setBar.calls.count()).toEqual(0); 25 26 foo.setBar(); 27 foo.setBar(); 28 29 expect(foo.setBar.calls.count()).toEqual(2); 30 }); 31 32 it("tracks the arguments of each call", function() { 33 foo.setBar(123); 34 foo.setBar(456, "baz"); 35 36 expect(foo.setBar.calls.argsFor(0)).toEqual([123]); 37 expect(foo.setBar.calls.argsFor(1)).toEqual([456, "baz"]); 38 }); 39 40 it("tracks the arguments of all calls", function() { 41 foo.setBar(123); 42 foo.setBar(456, "baz"); 43 44 expect(foo.setBar.calls.allArgs()).toEqual([[123],[456, "baz"]]); 45 }); 46 47 it("can provide the context and arguments to all calls", function() { 48 foo.setBar(123); 49 50 expect(foo.setBar.calls.all()).toEqual([{object: foo, args: [123], returnValue: undefined}]); 51 }); 52 53 it("has a shortcut to the most recent call", function() { 54 foo.setBar(123); 55 foo.setBar(456, "baz"); 56 57 expect(foo.setBar.calls.mostRecent()).toEqual({object: foo, args: [456, "baz"], returnValue: undefined}); 58 }); 59 60 it("has a shortcut to the first call", function() { 61 foo.setBar(123); 62 foo.setBar(456, "baz"); 63 64 expect(foo.setBar.calls.first()).toEqual({object: foo, args: [123], returnValue: undefined}); 65 }); 66 67 it("tracks the context", function() { 68 var spy = jasmine.createSpy('spy'); 69 var baz = { 70 fn: spy 71 }; 72 var quux = { 73 fn: spy 74 }; 75 baz.fn(123); 76 quux.fn(456); 77 78 expect(spy.calls.first().object).toBe(baz); 79 expect(spy.calls.mostRecent().object).toBe(quux); 80 }); 81 82 it("can be reset", function() { 83 foo.setBar(123); 84 foo.setBar(456, "baz"); 85 86 expect(foo.setBar.calls.any()).toBe(true); 87 88 foo.setBar.calls.reset(); 89 90 expect(foo.setBar.calls.any()).toBe(false); 91 }); 92 }); 93 })();
createSpy
假如沒有函數可以追蹤,我們可以自己創建一個空的Spy。創建后的Spy功能與其他的Spy一樣:跟蹤調用、參數等,但該Spy沒有實際的代碼實現,這種方式經常會用在對JavaScript中的對象的測試。
示例7:
1 (function(){ 2 describe("A spy, when created manually", function() { 3 var whatAmI; 4 5 beforeEach(function() { 6 whatAmI = jasmine.createSpy('whatAmI'); 7 8 whatAmI("I", "am", "a", "spy"); 9 }); 10 11 it("is named, which helps in error reporting", function() { 12 expect(whatAmI.and.identity()).toEqual('whatAmI'); 13 }); 14 15 it("tracks that the spy was called", function() { 16 expect(whatAmI).toHaveBeenCalled(); 17 }); 18 19 it("tracks its number of calls", function() { 20 expect(whatAmI.calls.count()).toEqual(1); 21 }); 22 23 it("tracks all the arguments of its calls", function() { 24 expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy"); 25 }); 26 27 it("allows access to the most recent call", function() { 28 expect(whatAmI.calls.mostRecent().args[0]).toEqual("I"); 29 }); 30 }); 31 })();
createSpyObj
如果需要spy模擬多個函數調用,可以向jasmine.createSpyObj中傳入一個字符串數組,它將返回一個對象,你所傳入的所有字符串都將對應一個屬性,每個屬性即為一個Spy。
示例8:
1 (function(){ 2 describe("Multiple spies, when created manually", function() { 3 var tape; 4 5 beforeEach(function() { 6 tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']); 7 8 tape.play(); 9 tape.pause(); 10 tape.rewind(0); 11 }); 12 13 it("creates spies for each requested function", function() { 14 expect(tape.play).toBeDefined(); 15 expect(tape.pause).toBeDefined(); 16 expect(tape.stop).toBeDefined(); 17 expect(tape.rewind).toBeDefined(); 18 }); 19 20 it("tracks that the spies were called", function() { 21 expect(tape.play).toHaveBeenCalled(); 22 expect(tape.pause).toHaveBeenCalled(); 23 expect(tape.rewind).toHaveBeenCalled(); 24 expect(tape.stop).not.toHaveBeenCalled(); 25 }); 26 27 it("tracks all the arguments of its calls", function() { 28 expect(tape.rewind).toHaveBeenCalledWith(0); 29 }); 30 }); 31 })();
關於createSpy和createSpyObj,讀到這里大家可能很難理解其真正的應用場景。不過沒關系,后續的例子中也包含了其用法,大家應該能慢慢理解如何運用它們。
其他匹配方式
jasmine.any
jasmine.any方法以構造器或者類名作為參數,Jasmine將判斷期望值和真實值的構造器是否相同,若相同則返回true。
示例9:
1 (function(){ 2 describe("jasmine.any", function() { 3 it("matches any value", function() { 4 expect({}).toEqual(jasmine.any(Object)); 5 expect(12).toEqual(jasmine.any(Number)); 6 }); 7 8 describe("when used with a spy", function() { 9 it("is useful for comparing arguments", function() { 10 var foo = jasmine.createSpy('foo'); 11 foo(12, function() { 12 return true; 13 }); 14 15 expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function)); 16 }); 17 }); 18 }); 19 })();
jasmine.anything
jamine.anything判斷只要不是null或undefined的值,若不是則返回true。
示例10:
1 (function(){ 2 describe("jasmine.anything", function() { 3 it("matches anything", function() { 4 expect(1).toEqual(jasmine.anything()); 5 }); 6 7 describe("when used with a spy", function() { 8 it("is useful when the argument can be ignored", function() { 9 var foo = jasmine.createSpy('foo'); 10 foo(12, function() { 11 return false; 12 }); 13 14 expect(foo).toHaveBeenCalledWith(12, jasmine.anything()); 15 }); 16 }); 17 }); 18 })();
jasmine.objectContaining
jasmine.objectContaining用來判斷對象中是否存在指定的鍵值屬性對。
示例11:
1 describe("jasmine.objectContaining", function() { 2 var foo; 3 4 beforeEach(function() { 5 foo = { 6 a: 1, 7 b: 2, 8 bar: "baz" 9 }; 10 }); 11 12 it("matches objects with the expect key/value pairs", function() { 13 expect(foo).toEqual(jasmine.objectContaining({ 14 bar: "baz" 15 })); 16 expect(foo).not.toEqual(jasmine.objectContaining({ 17 c: 37 18 })); 19 }); 20 21 describe("when used with a spy", function() { 22 it("is useful for comparing arguments", function() { 23 var callback = jasmine.createSpy('callback'); 24 25 callback({ 26 bar: "baz" 27 }); 28 29 expect(callback).toHaveBeenCalledWith(jasmine.objectContaining({ 30 bar: "baz" 31 })); 32 expect(callback).not.toHaveBeenCalledWith(jasmine.objectContaining({ 33 c: 37 34 })); 35 }); 36 }); 37 });
jasmine.arrayContaining
jasmine.arrayContaining可以用來判斷數組中是否有期望的值。
示例12:
1 (function(){ 2 describe("jasmine.arrayContaining", function() { 3 var foo; 4 5 beforeEach(function() { 6 foo = [1, 2, 3, 4]; 7 }); 8 9 it("matches arrays with some of the values", function() { 10 expect(foo).toEqual(jasmine.arrayContaining([3, 1])); // 直接在期望值中使用jasmine.arrayContaining達到目的 11 expect(foo).not.toEqual(jasmine.arrayContaining([6])); 12 }); 13 14 describe("when used with a spy", function() { 15 it("is useful when comparing arguments", function() { 16 var callback = jasmine.createSpy('callback'); // 創建一個空的Spy 17 18 callback([1, 2, 3, 4]); // 將數組內容作為參數傳入Spy中 19 20 expect(callback).toHaveBeenCalledWith(jasmine.arrayContaining([4, 2, 3])); 21 expect(callback).not.toHaveBeenCalledWith(jasmine.arrayContaining([5, 2])); 22 }); 23 }); 24 }); 25 })();
jasmine.stringMatching
jasmine.stringMatching用來模糊匹配字符串,在jasmine.stringMatching中也可以使用正則表達式進行匹配,使用起來非常靈活。
示例13:
1 describe('jasmine.stringMatching', function() { 2 it("matches as a regexp", function() { 3 expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)}); 4 expect({foo: 'foobarbaz'}).toEqual({foo: jasmine.stringMatching('bar')}); 5 }); 6 7 describe("when used with a spy", function() { 8 it("is useful for comparing arguments", function() { 9 var callback = jasmine.createSpy('callback'); 10 11 callback('foobarbaz'); 12 13 expect(callback).toHaveBeenCalledWith(jasmine.stringMatching('bar')); 14 expect(callback).not.toHaveBeenCalledWith(jasmine.stringMatching(/^bar$/)); 15 }); 16 }); 17 });
不規則匹配(自定義匹配):asymmetricMatch
某些場景下,我們希望能按照自己設計的規則進行匹配,此時我們可以自定義一個對象,該對象只要包含一個名為asymmetricMatch的方法即可。
示例14:
1 describe("custom asymmetry", function() { 2 var tester = { 3 asymmetricMatch: function(actual) { 4 var secondValue = actual.split(',')[1]; 5 return secondValue === 'bar'; 6 } 7 }; 8 9 it("dives in deep", function() { 10 expect("foo,bar,baz,quux").toEqual(tester); 11 }); 12 13 describe("when used with a spy", function() { 14 it("is useful for comparing arguments", function() { 15 var callback = jasmine.createSpy('callback'); 16 17 callback('foo,bar,baz'); 18 19 expect(callback).toHaveBeenCalledWith(tester); 20 }); 21 }); 22 });
注:示例中的asymmetricMatch方法使我們判斷字符串以','分割之后,index為1的內容為'bar'。
Jasmine Clock
Jasmine中可以使用jasmine.clock()方法來模擬操縱時間。
要想使用jasmine.clock(),先調用jasmine.clock().install告訴Jasmine你想要在spec或者suite操作時間,當你不需要使用時,務必調用jasmine.clock().uninstall來恢復時間狀態。
示例15:
1 (function(){ 2 describe("Manually ticking the Jasmine Clock", function() { 3 var timerCallback; 4 5 beforeEach(function() { 6 timerCallback = jasmine.createSpy("timerCallback"); 7 jasmine.clock().install(); 8 }); 9 10 afterEach(function() { 11 jasmine.clock().uninstall(); 12 }); 13 14 it("causes a timeout to be called synchronously", function() { 15 setTimeout(function() { 16 timerCallback(); 17 }, 100); 18 19 expect(timerCallback).not.toHaveBeenCalled(); 20 21 jasmine.clock().tick(101); 22 23 expect(timerCallback).toHaveBeenCalled(); 24 }); 25 26 it("causes an interval to be called synchronously", function() { 27 setInterval(function() { 28 timerCallback(); 29 }, 100); 30 31 expect(timerCallback).not.toHaveBeenCalled(); 32 33 jasmine.clock().tick(101); 34 expect(timerCallback.calls.count()).toEqual(1); 35 36 jasmine.clock().tick(50); 37 expect(timerCallback.calls.count()).toEqual(1); 38 39 jasmine.clock().tick(50); 40 expect(timerCallback.calls.count()).toEqual(2); 41 }); 42 43 describe("Mocking the Date object", function(){ 44 it("mocks the Date object and sets it to a given time", function() { 45 var baseTime = new Date(2013, 9, 23); 46 47 jasmine.clock().mockDate(baseTime); 48 49 jasmine.clock().tick(50); 50 expect(new Date().getTime()).toEqual(baseTime.getTime() + 50); 51 }); 52 }); 53 }); 54 })();
示例中使用jasmine.clock().tick(milliseconds)來控制時間前進,本例中出現了三種時間控制方式:
- setTimeout: 定期執行一次,當jasmine.clock().tick()的時間超過了timeout設置的時間時觸發
- setInterval: 定期循環執行,每當jasmine.clock().tick()的時間超過了timeout設置的時間時觸發
- mockDate: 模擬一個指定日期(當不提供基准時間參數時,以當前時間為基准時間)
異步支持
Jasmine可以支持spec中執行異步操作,當調用beforeEach, it和afterEach時,函數可以包含一個可選參數done,當spec執行完畢之后,調用done通知Jasmine異步操作已執行完畢。
示例16:
1 (function(){ 2 describe("Asynchronous specs", function() { 3 var value; 4 5 beforeEach(function(done) { 6 setTimeout(function() { 7 value = 0; 8 done(); 9 }, 1); 10 }); 11 12 // 在上面beforeEach的done()被執行之前,這個測試用例不會被執行 13 it("should support async execution of test preparation and expectations", function(done) { 14 value++; 15 expect(value).toBeGreaterThan(0); 16 done(); // 執行完done()之后,該測試用例真正執行完成 17 }); 18 19 // Jasmine異步執行超時時間默認為5秒,超過后將報錯 20 describe("long asynchronous specs", function() { 21 22 // 如果要調整指定用例的默認的超時時間,可以在beforeEach,it和afterEach中傳入一個時間參數 23 beforeEach(function(done) { 24 // setTimeout(function(){}, 2000); // 可以試試如果該方法執行超過1秒時js會報錯 25 done(); 26 }, 1000); 27 28 it("takes a long time", function(done) { 29 setTimeout(function() { 30 done(); 31 }, 9000); 32 }, 10000); 33 34 afterEach(function(done) { 35 done(); 36 }, 1000); 37 }); 38 }); 39 })();
關於用法在代碼中已經加了注釋,另外補充一點,如果需要設置全局的默認超時時間,可以設置jasmine.DEFAULT_TIMEOUT_INTERVAL的值。
當異步執行時間超過設置的執行超時時間js將會報錯:
至此為止,我們已經了解了Jasmine的所有用法,Jasmine的基本語法並不復雜,但是想要運用熟練還是需要在實際項目中慢慢實踐。
參考資料
Jasmine官方:http://jasmine.github.io/2.3/introduction.html
KeenWon:http://keenwon.com/1218.html