使用 Jasmine 進行測試驅動的 JavaScript 開發


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();
  });
});

 

RequireJS Jasmine 2.0 編寫測試

http://ju.outofmemory.cn/entry/96587


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM