JavaScript單元測試框架-Jasmine

Jasmine的開發團隊來自PivotalLabs,他們一開始開發的JavaScript測試框架是JsUnit,來源於著名的JAVA測試框架JUnit。JsUnit是xUnit的JavaScript實現。可是JsUnit在2009年後就已經中止維護了,他們推出了一個新的BDD框架Jasmine。Jasmine不依賴於任何框架,因此適用於全部的Javascript代碼。javascript

所謂BDD(行爲驅動開發,Behaviour Driven Development),是一種新的敏捷開發方法。Dan North對BDD給出的定義爲:css

BDD是第二代的、由外及內的、基於拉(pull)的、多方利益相關者的(stakeholder)、多種可擴展的、高自動化的敏捷方法。它描述了一個交互循環,能夠具備帶有良好定義的輸出(即工做中交付的結果):已測試過的軟件。

BDD與TDD(Test Driven Development )的主要區別是,使得非程序人員也能參與到測試用例的編寫中來,大大下降了客戶、用戶、項目管理者與開發者之間來回翻譯的成本。因此BDD更加註重業務需求而不是技術[1]。html

下載

在Jasmine的Github官方主頁:https://github.com/jasmine/jasmine
找到上方的releases,點擊會跳轉到https://github.com/jasmine/jasmine/releases。
下載已發佈的zip包,好比下載當前(2015-03-09)的最新版本爲:jasmine-standalone-2.2.0.zipjava

目錄結構

解壓以後,能夠看到有1個html文件和3個文件夾。git

  • 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,便可看到執行的結果。github

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),是由一組全局beforeEachafterEachbeforeAllafterAll函數來實現的。

  • beforeEach():在describe函數中每一個Spec執行以前執行。
  • afterEach(): 在describe函數中每一個Spec數執行以後執行。
  • beforeAll():在describe函數中全部的Specs執行以前執行,但只執行一次,在Sepc之間並不會被執行。
  • afterAll(): 在describe函數中全部的Specs執行以後執行,但只執行一次,在Sepc之間並不會被執行。

beforeAllafterAll適用於執行比較耗時或者耗資源的一些共同的初始化和清理工做。並且在使用時還要注意,它們不會在每一個Spec之間執行,因此不適用於每次執行前都須要乾淨環境的Spec。

this值

除了在describe函數開始定義變量,用於各it函數共享數據外,還能夠經過this關鍵字來共享數據。

在在每個Spec的生命週期(beforeEach->it->afterEach)的開始,都將有一個空的this對象(在開始下一個Spec週期時,this會被重置爲空對象)。

嵌套Suite

describe函數能夠嵌套,每層均可以定義Specs。這樣就可讓一個Suite由一組樹狀的方法組成。

每一個嵌套的describe函數,均可以有本身的beforeEachafterEach函數。
在執行每一個內層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語句表示,返回truefalseexpect語句有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的函數名以前添加xxit),那麼該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')來爲objfunction方法聲明一個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用於檢測實際值是否爲nullundefined,若是不爲nullundefined,則返回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用於setTimeoutsetInterval的回調控制,它使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,其累計設置的值達到setTimeoutsetInterval中指定的延時時間,則觸發回調函數。

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)

調用beforeEachit或者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
[2] 官方文檔introduction.js
[3] 官方文檔custom_matcher.js
[4] Jasmine——JavaScript 單元測試框架

相關文章
相關標籤/搜索