1. 程式人生 > >JavaScript單元測試框架-Jasmine

JavaScript單元測試框架-Jasmine

calling help without 還原 develop util 也不會 目錄結構 函數調用

轉載自金石開的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),是由一組全局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](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