[Testing] Jasmine — JavaScript 的 BDD 測試架構

Jasmine是一個針對Javascript編程語言的行為驅動測試框架。Jasmine 在程式的撰寫上和RSpec非常類似:

describe("Book list", function(){
it("returns empty if none matches", function(){
//...
});
it("returns matched items", function(){
$("#query").val("Javascript");
$("#search").click();
expect($("#results").find("li")).toEqual(5);
});
});

使用 Jasmine 的 HTML 外掛程式(SpecRunner.html),在執行時期會產生 HTML 報表。

設置 Jasmine

下載Jasmine最新的獨立發佈版本(standalone release),解壓縮後修改SpecRunner.html文件。

或可以搭配其他環境使用:

group :development, :test do
gem 'jasmine'
end

安裝後執行:

$ jasmine init
$ rake jasmine:ci

describe, it and expect

首先我們在spec資料夾下建立 hello.spec.js 檔案:

// spec/hello.spec.js
describe("Hello world", function() {
it("says hello", function() {
expect(helloWorld()).toEqual("Hello world!");
// 如果只需檢查是否包含world可以使用
// expect(helloWorld()).toContain("world");
});
});

jasmine 使用 describe 語法定義一個測試集(suite), 這個測試集的名字是 hello world。it區塊叫做一個參數規格(specification),或者簡寫為一個spec,用來描述你的組件應該做些什麼。我們期望helloWorld( )的輸出等於(toEqual)字符串「Hello World!」,toEqual 則是一個匹配器(matcher)

在SpecRunner.html中載入hello.spec.js

<script type="text/javascript" src="spec/hello.spec.js"></script>

此時運行SpecRunner.html會拋出錯誤,因爲我們還沒有實際代碼。接下來編寫實際代碼。在src資料夾下建立 hello.js

# src/hello.js
function helloWorld() {
return "Hello world!";
}

在SpecRunner.html中載入hello.js

<script type="text/javascript" src="src/hello.js"></script>

完成後測試將會通過。

主要原則

  • 猶豫就測試

編寫spec需要花費你大量的時間, 但可用來做測試的時間可能沒有那麼多。 如果你對是否應該對某個功能進行測試猶豫,那就寫測試吧。

  • 每個spec應該只測試一種情形或場景。
  • 不需要去測試私有方法。

Matchers

  • toEqual
expect([1, 2, 3]).toEqual([1, 2, 3]);
  • toBe(檢查是否爲同一對象)
var array = [1, 2, 3];
expect(array).toEqual([1, 2, 3]); // true
expect(array).toBe([1, 2, 3]); // false, 不是同一對象
  • toBeTruthy
  • toBeFalsy

在Jasmine中被視作falsy的東西(在Javascript中也一樣): false / 0 / “” / undefined / null / NaN

  • toContain
expect([1, 2, 3, 4]).toContain(3);
  • toBeDefined
  • toBeUndefined
  • toBeNull
  • toBeNaN
  • toBeGreaterThan
  • toBeLessThan
  • toBeCloseTo(可給定一個小數精確程度作為第二個參數)
expect(12.34).toBeCloseTo(12.3, 1);
  • toMatch(配合正則表達式)
expect("jasmine@example.com").toMatch("\w+@\w+\.\w+");
  • toThrow(是否拋出錯誤)

用not來否定匹配器

expect(foo).not.toEqual(bar);

Custom Matcher

beforeEach(function() {
this.addMatchers({
toBeLarge: function() {
this.message = function() {
return "Expected " + this.actual + " to be large";
};
return this.actual > 100;
}
});
});

每一個匹配器從expect函數接收一個參數,這個參數就是this.actual。this.message是當匹配器運行失敗時返回的訊息。

beforeEach / afterEach

可將每個spec執行前重複使用的code整理到beforeEach()。反之, 想要在每個spec之後執行一些代碼,只需要將代碼放入afterEach()中。

跳過Specs

  • 將 it 後面的 callback function 移除。
  • 可以使用 pending(); 語法來 pending 測試。
  • 或是在 it 或 describe 前加上x。
  • 使用return終止後面函數的執行

類型匹配

只關心類型是什麼,不在意回傳的值。

expect(rand()).toEqual(jasmine.any(Number));
  • jasmine.any(Number)
  • jasmine.any(String)
  • jasmine.any(Object)
  • jasmine.any(myObject)

SPY 功能

對程序進行監視。

var realBusiness = function(callback) {
$.ajax({
url: 'business.do',
success: function() {
callback(data);
}
})
};
realBusiness(function(list) {
$('#map').html(list);
});

如果實際的程式中有對外部應用的相依,但在測試中我們不希望真的發送請求的時候,可以用spy來解決這個問題。

使用spyOn(object, method); 來監視,接着toHaveBeenCalled(); 或是 toHaveBeenCalledWith(); 確保程式有被執行。

describe('realBusiness', function() {
it('sends request to server', function() {
spyOn($, 'ajax');
realBusiness(undefined);
expect($.ajax).toHaveBeenCalled();
});
it('calls callback when success', function(){
var callback = function(){
spyOn($, 'ajax').andCallFake(function(){
// callback
e.success({});
});
};
realBusiness(callback);
expect($.ajax.mostRecentCall.args[0].url).toEqual('business.do');
});
});

當 spy 在 $.ajax 上,後面的程式中,無論何處呼叫 $.ajax,Jasmine 都不會真的呼叫 $.ajax,而是呼叫這個 spy,而且會記錄所有的呼叫記錄。

Spy Methods

spyOn(events, 'publish');
spyOn(events, 'publish').and.callThrough(); // 實際執行
spyOn(events, 'publish').and.returnValue(false); //
確保返回一個具體值
spyOn(events, 'publish').and.callFake(function(name, args) {
window.alert(name);
}); //
調用一個偽函數
spyOn(events, 'publish').and.throwError('oops'); // 確保拋出錯誤訊息
spyOn(events, 'publish').and.stub(); // set a spy back to a stub method

參考: