JavaScript單元測試框架-Jasmine
轉載自金石開的blog:http://www.cnblogs.com/zhcncn/p/4330112.html
Jasmine的開發團隊來自PivotalLabs,他們一開始開發的JavaScript測試框架是JsUnit,來源於著名的JAVA測試框架JUnit。JsUnit是xUnit的JavaScript實現。但是JsUnit在2009年後就已經停止維護了,他們推出了一個新的BDD框架Jasmine。Jasmine不依賴於任何框架,所以適用於所有的Javascript代碼。
所謂BDD(行為驅動開發,Behaviour Driven Development),是一種新的敏捷開發方法。Dan North對BDD給出的定義為:
BDD是第二代的、由外及內的、基於拉(pull)的、多方利益相關者的(stakeholder)、多種可擴展的、高自動化的敏捷方法。它描述了一個交互循環,可以具有帶有良好定義的輸出(即工作中交付的結果):已測試過的軟件。
BDD與TDD(Test Driven Development )的主要區別是,使得非程序人員也能參與到測試用例的編寫中來,大大降低了客戶、用戶、項目管理者與開發者之間來回翻譯的成本。所以BDD更加註重業務需求而不是技術[1]。
下載
在Jasmine的Github官方主頁:https://github.com/jasmine/jasmine
找到上方的releases,點擊會跳轉到https://github.com/jasmine/jasmine/releases。
下載已發布的zip包,比如下載當前(2015-03-09)的最新版本為:jasmine-standalone-2.2.0.zip
目錄結構
解壓之後,可以看到有1個html文件和3個文件夾。
- lib:存放了運行測試案例所必須的文件,其內包含
jasmine-2.2.0
文件夾。可以將不同版本的Jasmine放在lib下,以便使用時切換。- jasmine.js:整個框架的核心代碼。
- jasmine-html.js:用來展示測試結果的js文件。
- boot.js:jasmine框架的的啟動腳本。需要註意的是,這個腳本應該放在jasmine.js之後,自己的js測試代碼之前加載。
- jasmine.css:用來美化測試結果。
- spec:存放測試腳本。
- PlayerSpec.js:就是針對src文件夾下的Player.js所寫的測試用例。
- SpecHelper.js:用來添加自定義的檢驗規則,如果框架本身提供的規則(諸如
toBe
,toNotBe
等)不適用,就可以額外添加自己的規則(在本文件中添加了自定義的規則toBePlaying
)。
- src:存放需要測試的js文件。Jasmine提供了一個Example(Player.js,Song.js)。
- SpecRunner.html:運行測試用例的環境。它將上面3個文件夾中一些必要的文件都包含了進來。如果你想將自己的測試添加進來的話,那麽就修改相應的路徑。
其中,spec文件夾,src文件夾和SpecRunner.html文件是Jasmine提供的一個完整示例,用瀏覽器打開 SpecRunner.html,即可看到執行的結果。
SpecRunner.html
運行測試用例的例子:
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.2.0</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.2.0/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.2.0/jasmine.css">
<script src="lib/jasmine-2.2.0/jasmine.js"></script>
<script src="lib/jasmine-2.2.0/jasmine-html.js"></script>
<script src="lib/jasmine-2.2.0/boot.js"></script>
<!-- include source files here... -->
<script src="src/Player.js"></script>
<script src="src/Song.js"></script>
<!-- include spec files here... -->
<script src="spec/SpecHelper.js"></script>
<script src="spec/PlayerSpec.js"></script>
</head>
<body></body>
</html>
核心概念
框架中的一些核心概念,可以參考官方文檔中的介紹[2]。下面進入搬磚模式:
Suites
Suite表示一個測試集,以函數describe(string, function)
封裝,它包含2個參數:string
:測試組名稱,function
:測試組函數。
一個Suite(describe
)包含多個Specs(it
),一個Specs(it
)包含多個斷言(expect
)。
Setup和Teardown操作
Jasmine的Setup和Teardown操作(Setup在每個測試用例Spec執行之前做一些初始化操作,Teardown在每個Sepc執行完之後做一些清理操作,這兩個函數名稱來自於JUnit),是由一組全局beforeEach
,afterEach
, beforeAll
,afterAll
函數來實現的。
- beforeEach():在
describe
函數中每個Spec執行之前執行。 - afterEach(): 在
describe
函數中每個Spec數執行之後執行。 - beforeAll():在
describe
函數中所有的Specs執行之前執行,但只執行一次,在Sepc之間並不會被執行。 - afterAll(): 在
describe
函數中所有的Specs執行之後執行,但只執行一次,在Sepc之間並不會被執行。
beforeAll
和 afterAll
適用於執行比較耗時或者耗資源的一些共同的初始化和清理工作。而且在使用時還要註意,它們不會在每個Spec之間執行,所以不適用於每次執行前都需要幹凈環境的Spec。
this值
除了在describe
函數開始定義變量,用於各it
函數共享數據外,還可以通過this
關鍵字來共享數據。
在在每一個Spec的生命周期(beforeEach->it->afterEach
)的開始,都將有一個空的this
對象(在開始下一個Spec周期時,this
會被重置為空對象)。
嵌套Suite
describe
函數可以嵌套,每層都可以定義Specs。這樣就可以讓一個Suite由一組樹狀的方法組成。
每個嵌套的describe
函數,都可以有自己的beforeEach
,afterEach
函數。
在執行每個內層Spec時,都會按嵌套的由外及內的順序執行每個beforeEach
函數,所以內層Sepc可以訪問到外層Sepc中的beforeEach
中的數據。類似的,當內層Spec執行完成後,會按由內及外的順序執行每個afterEach
函數。
describe("A spec", function() {
var foo;
beforeEach(function() {
foo = 0;
foo += 1;
});
afterEach(function() {
foo = 0;
});
it("is just a function, so it can contain any code", function() {
expect(foo).toEqual(1);
});
it("can have more than one expectation", function() {
expect(foo).toEqual(1);
expect(true).toEqual(true);
});
describe("nested inside a second describe", function() {
var bar;
beforeEach(function() {
bar = 1;
});
it("can reference both scopes as needed", function() {
expect(foo).toEqual(bar);
});
});
});
Specs
Spec表示測試用例,以it(string, function)
函數封裝,它也包含2個參數:string
:測試用例名稱,function
:測試用例函數。
Expectations
Expectation就是一個斷言,以expect
語句表示,返回true
或false
。expect
語句有1個參數,代表要測試的實際值(the actual)。
只有當一個Spec中的所有Expectations全為ture
時,這個Spec才通過,否則失敗。
Expectation帶實際值,它和表示匹配規則的Matcher鏈接在一起,Matcher帶有期望值。
Matchers
Matcher實現了斷言的比較操作,將Expectation傳入的實際值和Matcher傳入的期望值比較。
任何Matcher都能通過在expect
調用Matcher前加上not
來實現一個否定的斷言(expect(a).not().toBe(false);
)。
常用的Matchers有:
- toBe():相當於
===
比較。 - toNotBe()
- toBeDefined():檢查變量或屬性是否已聲明且賦值。
- toBeUndefined()
- toBeNull():是否是
null
。 - toBeTruthy():如果轉換為布爾值,是否為
true
。 - toBeFalsy()
- toBeLessThan():數值比較,小於。
- toBeGreaterThan():數值比較,大於。
-
toEqual():相當於
==
,註意與toBe()
的區別。
一個新建的Object不是(not to be)另一個新建的Object,但是它們是相等(to equal)的。expect({}).not().toBe({}); expect({}).toEqual({});
- toNotEqual()
- toContain():數組中是否包含元素(值)。只能用於數組,不能用於對象。
-
toBeCloseTo():數值比較時定義精度,先四舍五入後再比較。
it("The ‘toBeCloseTo‘ matcher is for precision math comparison", function() { var pi = 3.1415926, e = 2.78; expect(pi).not.toBeCloseTo(e, 2); expect(pi).toBeCloseTo(e, 0); });
- toHaveBeenCalled()
- toHaveBeenCalledWith()
- toMatch():按正則表達式匹配。
- toNotMatch()
-
toThrow():檢驗一個函數是否會拋出一個錯誤
自定義Matchers的實現
自定義Matcher(被稱為Matcher Factories)實質上是一個函數(該函數的參數可以為空),該函數返回一個閉包,該閉包的本質是一個compare
函數,compare
函數接受2個參數:actual value 和 expected value。
compare
函數必須返回一個帶pass
屬性的結果Object,pass
屬性是一個Boolean值,表示該Matcher的結果(為true
表示該Matcher實際值與預期值匹配,為false
表示不匹配),也就是說,實際值與預期值具體的比較操作的結果,存放於pass
屬性中。
最後測試輸出的失敗信息應該在返回結果Object中的message
屬性中來定義。
var customMatchers = {
toBeGoofy: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
if (expected === undefined) {
expected = ‘‘;
}
var result = {};
result.pass = util.equals(actual.hyuk, "gawrsh" + expected, customEqualityTesters);
if (result.pass) {
result.message = "Expected " + actual + " not to be quite so goofy";
} else {
result.message = "Expected " + actual + " to be goofy, but it was not very goofy";
}
return result;
}
};
}
};
自定義Matchers的使用
對自定義Matcher有2種使用方法:
- 將該函數添加到一個特定的
describe
函數的beforeEach
中,以便該describe
函數中的所有Spec都能調用到它。但其他describe
中並不能使用該Matcher。
該方法的例子可以參考官網提供的custom_matcher.js
的實現[3]。
describe("Custom matcher: ‘toBeGoofy‘", function() {
beforeEach(function() {
jasmine.addMatchers(customMatchers);
});
it("can take an ‘expected‘ parameter", function() {
expect({
hyuk: ‘gawrsh is fun‘
}).toBeGoofy(‘ is fun‘);
});
});
- 將該函數添加到全局的
beforeEach
函數中,這樣所有的Suites中的所有的Specs,都可以使用該Matcher。
該方法的例子可以參考Jasmine提供的Demo中的SpecHelper.js
文件中的toBePlaying
自定義的規則的實現。
//定義
beforeEach(function () {
jasmine.addMatchers({
toBePlaying: function () {
// 自定義Matcher:toBePlaying
return {
//要返回的compare函數
compare: function (actual, expected) {
var player = actual;
//compare函數中要返回的結果Object,這裏是一個匿名Object,包含一個pass屬性。
return {
pass: player.currentlyPlayingSong === expected && player.isPlaying
}
}
};
}
});
});
//使用
describe("Player", function() {
it("should be able to play a Song", function() {
player.play(song);
//demonstrates use of custom matcher
expect(player).toBePlaying(song);
});
describe("when song has been paused", function() {
it("should indicate that the song is currently paused", function() {
// demonstrates use of ‘not‘ with a custom matcher
expect(player).not.toBePlaying(song);
});
)};
禁用Suites
Suites可以被Disabled。在describe
函數名之前添加x
即可將Suite禁用。
被Disabled的Suites在執行中會被跳過,該Suite的結果也不會顯示在結果集中。
xdescribe("A spec", function() {
var foo;
beforeEach(function() {
foo = 0;
foo += 1;
});
it("is just a function, so it can contain any code", function() {
expect(foo).toEqual(1);
});
});
掛起Specs
有3種方法可以將一個Spec標記為Pending。被Pending的Spec不會被執行,但是Spec的名字會在結果集中顯示,只是標記為Pending。
- 如果在Spec函數
it
的函數名之前添加x
(xit
),那麽該Spec就會被標記為Pending。 - 一個沒有定義函數體的Sepc也會在結果集中被標記為Pending。
- 如果在Spec的函數體中調用
pending()
函數,那麽該Spec也會被標記為Pending。pending()
函數接受一個字符串參數,該參數會在結果集中顯示在PENDING WITH MESSAGE:
之後,作為為何被Pending的原因。
describe("Pending specs", function() {
xit("can be declared ‘xit‘", function() {
expect(true).toBe(false);
});
it("can be declared with ‘it‘ but without a function");
it("can be declared by calling ‘pending‘ in the spec body", function() {
expect(true).toBe(false);
pending(‘this is why it is pending‘);
});
});
高級特性
Spy
Spy能監測任何function的調用和方法參數的調用痕跡。需使用2個特殊的Matcher:
toHaveBeenCalled
:可以檢查function是否被調用過,toHaveBeenCalledWith
: 可以檢查傳入參數是否被作為參數調用過。
spyOn
使用spyOn(obj,‘function‘)
來為obj
的function
方法聲明一個Spy。不過要註意的一點是,對Spy函數的調用並不會影響真實的值。
describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
spyOn(foo, ‘setBar‘);
foo.setBar(123);
foo.setBar(456, ‘another param‘);
});
it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled();
});
it("tracks all the arguments of its calls", function() {
expect(foo.setBar).toHaveBeenCalledWith(123);
expect(foo.setBar).toHaveBeenCalledWith(456, ‘another param‘);
});
it("stops all execution on a function", function() {
// Spy的調用並不會影響真實的值,所以bar仍然是null。
expect(bar).toBeNull();
});
});
and.callThrough
如果在spyOn
之後鏈式調用and.callThrough
,那麽Spy除了跟蹤所有的函數調用外,還會直接調用函數額真實實現,因此Spy返回的值就是函數調用後實際的值了。
...
spyOn(foo, ‘getBar‘).and.callThrough();
foo.setBar(123);
fetchedBar = foo.getBar();
it("tracks that the spy was called", function() {
expect(foo.getBar).toHaveBeenCalled();
});
it("should not effect other functions", function() {
expect(bar).toEqual(123);
});
it("when called returns the requested value", function() {
expect(fetchedBar).toEqual(123);
});
});
and.stub
在調用and.callThrough後,如果你想阻止spi繼續對實際值產生影響,你可以調用and.stub。也就是說,and.stub是將spi對實際實現的影響還原到最終的狀態——不影響實際值。
spyOn(foo, ‘setBar‘).and.callThrough();
foo.setBar(123);
// 實際的bar=123
expect(bar).toEqual(123);
// 調用and.stub()後,之後調用foo.setBar將不會影響bar的值。
foo.setBar.and.stub();
foo.setBar(456);
expect(bar).toBe(123);
bar = null;
foo.setBar(123);
expect(bar).toBe(null);
全局匹配謂詞
jasmine.any
jasmine.any
的參數為一個構造函數,用於檢測該參數是否與實際值所對應的構造函數相匹配。
describe("jasmine.any", function() {
it("matches any value", function() {
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
});
describe("when used with a spy", function() {
it("is useful for comparing arguments", function() {
var foo = jasmine.createSpy(‘foo‘);
foo(12, function() {
return true;
});
expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
});
});
});
jasmine.anything
jasmine.anything
用於檢測實際值是否為null
或undefined
,如果不為null
或undefined
,則返回true
。
it("matches anything", function() {
expect(1).toEqual(jasmine.anything());
});
jasmine.objectContaining
用於檢測實際Object值中是否存在特定key/value對。
var foo;
beforeEach(function() {
foo = {
a: 1,
b: 2,
bar: "baz"
};
});
it("matches objects with the expect key/value pairs", function() {
expect(foo).toEqual(jasmine.objectContaining({
bar: "baz"
}));
expect(foo).not.toEqual(jasmine.objectContaining({
c: 37
}));
});
jasmine.arrayContaining
用於檢測實際Array值中是否存在特定值。
var foo;
beforeEach(function() {
foo = [1, 2, 3, 4];
});
it("matches arrays with some of the values", function() {
expect(foo).toEqual(jasmine.arrayContaining([3, 1]));
expect(foo).not.toEqual(jasmine.arrayContaining([6]));
});
Jasmine Clock
Jasmine Clock用於setTimeout
和setInterval
的回調控制,它使timer
的回調函數同步化,不再依賴於具體的時間,而是將時間離散化,使測試人員能精確控制具體的時間點。
安裝與卸載
調用jasmine.clock().install()
可以在特定的需要操縱時間的Spec或者Suite中安裝Jasmine Clock,註意操作完後要調用jasmine.clock().uninstall()
進行卸載。
var timerCallback;
beforeEach(function() {
timerCallback = jasmine.createSpy("timerCallback");
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
模擬超時(Mocking Timeout)
可以調用jasmine.clock().tick(nTime)
來模擬計時,一旦tick
中設置的時間nTime
,其累計設置的值達到setTimeout
或setInterval
中指定的延時時間,則觸發回調函數。
it("causes an interval to be called synchronously", function() {
setInterval(function() {
timerCallback();
}, 100);
expect(timerCallback).not.toHaveBeenCalled();
jasmine.clock().tick(101);
expect(timerCallback.calls.count()).toEqual(1);
jasmine.clock().tick(50);
expect(timerCallback.calls.count()).toEqual(1);
//tick設置的時間,累計到此201ms,因此會觸發setInterval中的毀掉函數被調用2次。
jasmine.clock().tick(50);
expect(timerCallback.calls.count()).toEqual(2);
});
異步支持(Asynchronous Support)
調用beforeEach
,it
或者afterEach
時,可以添加一個可選參數(Function
類型,在官方文檔的例子中該參數為done
)。當done
函數被調用,表明異步操作的回調函數調用成功;否則如果沒有調用done
,表明異步操作的回調函數調用失敗,則該Spec不會被調用,且會因為超時退出。
Jasmine等待異步操作完成的默認時間是5s,如果5s內異步操作沒有完成,則Spec會因為超時退出。超時時間也可以通過全局的jasmine.DEFAULT_TIMEOUT_INTERVAL
修改[4]。
var value;
// setTimeout代表一個異步操作。
beforeEach(function(done) {
setTimeout(function() {
value = 0;
// 調用done表示回調成功,否則超時。
done();
}, 1);
});
// 如果在beforeEach中的setTimeout的回調中沒有調用done,最終導致下面的it因超時而失敗。
it("should support async execution of test preparation and expectations", function(done) {
value++;
expect(value).toBeGreaterThan(0);
done();
});
參考資料
[1] [Javascript的Unit Test](http://www.tychio.net/tech/2013/07/10/unit-test.html)。
[2] [官方文檔introduction.js](http://jasmine.github.io/2.2/introduction.html)
[3] [官方文檔custom_matcher.js](http://jasmine.github.io/2.2/custom_matcher.html)
[4] [Jasmine——JavaScript 單元測試框架](http://inching.org/2014/03/05/javascript-jasmine/)
JavaScript單元測試框架-Jasmine