Jasmine 為 JavaScript 提供了 TDD (測試驅動開發)的框架,對於前端軟件開發提供了良好的質量保證,這里對 Jasmine 的配置和使用做一個說明。
目前,Jasmine 的最新版本是 2.3 版,這里以 2.3 版進行說明。網上已經有一些關於 Jasmine 的資料,但是,有些資料比較久遠,已經與現有版本不一致。所以,這里特別以最新版進行說明。
1. 下載
官網地址:http://jasmine.github.io/
官網文檔地址:http://jasmine.github.io/2.3/introduction.html
下載地址:https://github.com/jasmine/jasmine/releases
在 GitHub 上提供了獨立版本 jasmine-standalone-2.3.4.zip 和源碼版本,如果使用的話,直接使用 standalone 版本即可。
解壓之后,可以得到如下所示的文件結構。

其中,lib 中是 Jasmine 的實現文件,在 lib/jasmine-2.3.4 文件夾中,可以看到如下的文件。

打開最外層的 SpecRunner.html ,這是一個 Jasmine 的模板,其中提供了測試的示例,我們可以在使用中直接套用這個模板。其中的內容為:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Jasmine Spec Runner v2.3.4</title> <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.3.4/jasmine_favicon.png"> <link rel="stylesheet" href="lib/jasmine-2.3.4/jasmine.css"> <script src="lib/jasmine-2.3.4/jasmine.js"></script> <script src="lib/jasmine-2.3.4/jasmine-html.js"></script> <script src="lib/jasmine-2.3.4/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>
可以看到其中引用了 lib/jasmine-2.3.4/jasmine.js, lib/jasmine-2.3.4/jasmine-html.js 和 lib/jasmine-2.3.4/boot.js 三個系統文件,其中 boot.js 是網頁情況下的啟動文件,在 張丹 的 jasmine行為驅動,測試先行 這篇文章中,要寫一個 report.js 的啟動腳本,這里已經不用了,直接使用 boot.js 就可以。
頁面下面引用的 src/Player.js 和 src/Song.js 是我們的測試對象,而 spec/SpecHelper.js 和 spec/PlayerSpec.js 則是兩個對應的測試文件,測試用例就定義在 spec 中。
2. 測試的定義
我們還是直接看示例,
一個是 Song.js,這里定義了一個 Song 的類,通過原型定義了一個persistFavoriteStatus 實例方法,注意,這里還沒有實現,如果調用則會拋出異常。腳本如下。
function Song() { } Song.prototype.persistFavoriteStatus = function(value) { // something complicated throw new Error("not yet implemented"); };
另外一個是 player.js,定義了 Player 類,定義了一個歌手,通過原型定義了 play, pause, resume 和 makeFavorite 實例方法。對象有一個 isPlaying 的狀態,其中 resume 還沒有完成。
function Player() { } Player.prototype.play = function(song) { this.currentlyPlayingSong = song; this.isPlaying = true; }; Player.prototype.pause = function() { this.isPlaying = false; }; Player.prototype.resume = function() { if (this.isPlaying) { throw new Error("song is already playing"); } this.isPlaying = true; }; Player.prototype.makeFavorite = function() { this.currentlyPlayingSong.persistFavoriteStatus(true); };
下面看測試的定義,具體測試的說明,直接加在注釋中。
describe("Player", function() {
var player;
var song;
beforeEach(function() {
player = new Player();
song = new Song();
});
// 檢測正在歌手進行的歌曲確實是指定的歌曲
it("should be able to play a Song", function() {
player.play(song);
expect(player.currentlyPlayingSong).toEqual(song);
//demonstrates use of custom matcher
expect(player).toBePlaying(song);
});
// 進行測試的分組,這里測試暫停狀態
describe("when song has been paused", function() {
beforeEach(function() {
player.play(song);
player.pause();
});
// isPlaying 的狀態檢測
it("should indicate that the song is currently paused", function() {
expect(player.isPlaying).toBeFalsy();
// demonstrates use of 'not' with a custom matcher
//
expect(player).not.toBePlaying(song);
});
// 恢復
it("should be possible to resume", function() {
player.resume();
expect(player.isPlaying).toBeTruthy();
expect(player.currentlyPlayingSong).toEqual(song);
});
});
// demonstrates use of spies to intercept and test method calls
// 使用 spyOn 為對象創建一個 mock 函數
it("tells the current song if the user has made it a favorite", function() {
spyOn(song, 'persistFavoriteStatus');
player.play(song);
player.makeFavorite();
expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
});
//demonstrates use of expected exceptions
// 異常檢測
describe("#resume", function() {
it("should throw an exception if song is already playing", function() {
player.play(song);
expect(function() {
player.resume();
}).toThrowError("song is already playing");
});
});
});
使用瀏覽器直接打開 SpenRunner.html 看到的結果

可以看到測試都通過了。
如果我們將第一個測試 expect(player.currentlyPlayingSong).toEqual(song); 改成 expect(player.currentlyPlayingSong).toEqual( 1 );
測試通不過,顯示會變成這樣。

3. 語法
3.1 describe 和 it
describe 用來對測試用例進行分組,分組可以嵌套,每個分組可以有一個描述說明,這個說明將會出現在測試結果的頁面中。
describe("Player", function() {
describe("when song has been paused", function() {
而 it 就是測試用例,每個測試用例有一個字符串的說明,匿名函數內就是測試內容。
// 檢測正在歌手進行的歌曲確實是指定的歌曲 it("should be able to play a Song", function() { player.play(song); expect(player.currentlyPlayingSong).toEqual(song); });
測試結果的斷言使用 expect 進行,函數內提供測試的值,toXXX 中則是期望的值。
上面的測試使用 toEqual 進行相等斷言判斷。
3.2 beforeEach 和 afterEach
示例中還出現了 beforeEach。
var player; var song; beforeEach(function() { player = new Player(); song = new Song(); });
顧名思義,它表示在本組的每個測試之前需要進行的准備工作。在我們這里的測試中,總要用到 player 和 song 這兩個對象實例,使用 forEach 保證在每個測試用例執行之前,重新對這兩個對象進行了初始化。
afterEach 會在每一個測試用例執行之后執行。
3.3 自定義的斷言
除了系統定義的 toEqual 等等斷言之外,也可以使用自定義的斷言,在上面的示例中就出現了 toBePlaying 斷言。
//demonstrates use of custom matcher expect(player).toBePlaying(song);
這個自定義的斷言定義在 SpecHelper.js 文件中。
beforeEach(function () { jasmine.addMatchers({ toBePlaying: function () { return { compare: function (actual, expected) { var player = actual; return { pass: player.currentlyPlayingSong === expected && player.isPlaying }; } }; } }); });
其中調用了 jasmine 的 addMatchers 函數進行定義,原來這里不叫斷言,稱為 matcher ,也就是匹配器。
斷言是一個函數,返回一個對象,其中有一個 compare 的函數,這個函數接收兩個參數,第一個是實際值,第二個為期望的值。具體的斷言邏輯自己定義,這里比較歌手演唱的對象是否為我們傳遞的對象,並且歌手的狀態為正在表演中。
斷言函數需要返回一個對象,對象的 pass 屬性為一個 boolean 值,表示是否通過。
4. 常用斷言
4.1 toEqual
深相等,對於對象來說,會比較對象的每個屬性。對於數組來說,會比較數組中每個元素。
describe("The 'toEqual' matcher", function() {
it("works for simple literals and variables", function() {
var a = 12;
expect(a).toEqual(12);
});
it("should work for objects", function() {
var foo = {
a: 12,
b: 34
};
var bar = {
a: 12,
b: 34
};
expect(foo).toEqual(bar);
});
});
4.2 toBe
對於對象,引用相等。對於值,值相等。
pass: actual === expected
例如
it("and has a positive case", function() {
expect(true).toBe(true);
});
4.3 toBeTruthy
是否為真。
it("The 'toBeTruthy' matcher is for boolean casting testing", function() {
var a, foo = "foo";
expect(foo).toBeTruthy();
expect(a).not.toBeTruthy();
});
4.4 toBeFalsy
是否為假。
it("The 'toBeFalsy' matcher is for boolean casting testing", function() {
var a, foo = "foo";
expect(a).toBeFalsy();
expect(foo).not.toBeFalsy();
});
4.5 toBeDefined
是否定義過
it("creates spies for each requested function", function() {
expect(tape.play).toBeDefined();
expect(tape.pause).toBeDefined();
expect(tape.stop).toBeDefined();
expect(tape.rewind).toBeDefined();
});
4.6 toBeUndefined
沒有定義
it("The `toBeUndefined` matcher compares against `undefined`", function() {
var a = {
foo: "foo"
};
expect(a.foo).not.toBeUndefined();
expect(a.bar).toBeUndefined();
});
4.7 toBeNull
it("The 'toBeNull' matcher compares against null", function() {
var a = null;
var foo = "foo";
expect(null).toBeNull();
expect(a).toBeNull();
expect(foo).not.toBeNull();
});
4.9 toBeGreaterThan
it("The 'toBeGreaterThan' matcher is for mathematical comparisons", function() {
var pi = 3.1415926,
e = 2.78;
expect(pi).toBeGreaterThan(e);
expect(e).not.toBeGreaterThan(pi);
});
4.10 toBeLessThan
it("The 'toBeLessThan' matcher is for mathematical comparisons", function() {
var pi = 3.1415926,
e = 2.78;
expect(e).toBeLessThan(pi);
expect(pi).not.toBeLessThan(e);
});
4.11 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);
});
4.12 toContain
集合中是否包含。
it("The 'toContain' matcher is for finding an item in an Array", function() {
var a = ["foo", "bar", "baz"];
expect(a).toContain("bar");
expect(a).not.toContain("quux");
});
4.13 toMatch
正則表達式的匹配
it("The 'toMatch' matcher is for regular expressions", function() {
var message = "foo bar baz";
expect(message).toMatch(/bar/);
expect(message).toMatch("bar");
expect(message).not.toMatch(/quux/);
});
4.14 toThrow
檢測是否拋出異常
it("The 'toThrow' matcher is for testing if a function throws an exception", function() {
var foo = function() {
return 1 + 2;
};
var bar = function() {
return a + 1;
};
expect(foo).not.toThrow();
expect(bar).toThrow();
});
4.15 toHaveBeenCalled
4.16 toHaveBeenCalledWith
是否調用過。
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() {
expect(bar).toBeNull();
});
});

