一、jasmine 简介

Jasmine就是一个行动驱动开发模式的JS的单元测试工具。

Suites(describe)是Jasmine的核心,是一个测试集,里面包括多个specs(it),而每个specs里面可能包含多个断言(expect)。

搭建javascript单元测试项目 js测试框架_js单元测试

  • jasmine.js:整个框架的核心代码。
  • jasmine-html.js:用来展示测试结果的js文件。
  • boot.js:jasmine框架的的启动脚本。需要注意的是,这个脚本应该放在jasmine.js之后,自己的js测试代码之前加载。 
  • jasmine.css:用来美化测试结果。

在html页面引入以上文件,则可以进行jsamine测试

搭建javascript单元测试项目 js测试框架_测试用例_02

player.js为需要测试的文件

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

playerspec.js   针对Player.js所写的测试用例

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

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

核心概念

1、describe("Player", function() {})

Suite表示一个测试集,以函数describe(string, function)封装,它包含2个参数:
string:测试组名称,
function:测试组函数。

一个Suite(describe)包含多个Specs(it),一个Specs(it)包含多个断言(expect)。

2、Setup和Teardown操作/4个全局函数

为了能够在测试开始前先进行部分初始化,或者在测试结束后对一些内容进行销毁,主体还包括四个全局函数:
beforeEach():在describe函数中每个Spec执行之前执行。
afterEach():在describe函数中每个Spec数执行之后执行。
beforeAll():在describe函数中所有的Specs执行之前执行,但只执行一次,在Sepc之间并不会被执行。
afterAll():在describe函数中所有的Specs执行之后执行,但只执行一次,在Sepc之间并不会被执行。
上例中即在beforeEach()中new 了一个构造函数。

3、 it("should be able to play a Song", function() {})

Spec表示测试用例,以it(string, function)函数封装,它也包含2个参数:
string:测试用例名称,
function:测试用例函数。

4、expect(player.currentlyPlayingSong).toEqual(song);

Expectation就是一个断言,以expect语句表示,返回truefalseexpect语句有1个参数,代表要测试的实际值(the actual)。

只有当一个Spec中的所有Expectations全为ture时,这个Spec才通过,否则失败。

5、 expect(player.isPlaying).toBeFalsy();

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

6、 expect(player).toBePlaying(song);

  • toHaveBeenCalled()
  • toHaveBeenCalledWith()
  • toMatch():按正则表达式匹配。
  • toNotMatch()

 注意到在player.js中并没有toBeplaying()这一功能函数,这是一个自定义函数。

自定义Matcher(被称为Matcher Factories)实质上是一个函数(该函数的参数可以为空),该函数返回一个闭包,该闭包的本质是一个compare函数,compare函数接受2个参数:actual value 和 expected value。

compare函数必须返回一个带pass属性的结果Object,pass属性是一个Boolean值,表示该Matcher的结果(为true表示该Matcher实际值与预期值匹配,为false表示不匹配),也就是说,实际值与预期值具体的比较操作的结果,存放于pass属性中。

最后测试输出的失败信息应该在返回结果Object中的message属性中来定义。

beforeEach(function () {
  jasmine.addMatchers({
    toBePlaying: function () {
      return {
        compare: function (actual, expected) {
          var player = actual;
          return {
            pass: player.currentlyPlayingSong === expected && player.isPlaying
          };
        }
      };
    }
  });
});

对于本示例来说: actual为player,expected 为son

分析pass 属性的值:player.currentlyPlayingSong为song ===song &&前为true。player.isPlaying 在调用player.play(song)时,this.isPlaying被处置为true。所以Pass属性值为true。

7、禁用suites

Suites可以被Disabled。在describe函数名之前添加x即可将Suite禁用。

8、挂起Spec

有3种方法可以将一个Spec标记为Pending。被Pending的Spec不会被执行,但是Spec的名字会在结果集中显示,只是标记为Pending。

  • 如果在Spec函数it的函数名之前添加xxit),那么该Spec就会被标记为Pending。
  • 一个没有定义函数体的Sepc也会在结果集中被标记为Pending。
  • 如果在Spec的函数体中调用pending()函数,那么该Spec也会被标记为Pending。pending()函数接受一个字符串参数,该参数会在结果集中显示在PENDING WITH MESSAGE:之后,作为为何被Pending的原因。
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');
  });

9、全局匹配谓词

名称

用法

jasmine.any

参数为一个构造函数,用于检测该参数是否与实际值所对应的构造函数相匹配。

jasmine.anything

用于检测实际值是否为nullundefined,如果不为nullundefined,则返回true

jasmine.objectContaining

用于检测实际Object值中是否存在特定key/value对。

jasmine.arrayContaining

用于检测实际Array值中是否存在特定值。

expect({}).toEqual(jasmine.any(Object));
 expect(12).toEqual(jasmine.any(Number));
 expect(1).toEqual(jasmine.anything());

10、异步支持(Asynchronous Support)

调用beforeEachit或者afterEach时,可以添加一个可选参数(Function类型,在官方文档的例子中该参数为done)。当done函数被调用,表明异步操作的回调函数调用成功;否则如果没有调用done,表明异步操作的回调函数调用失败,则该Spec不会被调用,且会因为超时退出。

二、jasmine实例

describe('Take out food', function () {

  it('should generate best charge when best is 指定菜品半价', function() {
    let inputs = ["ITEM0001 x 1", "ITEM0013 x 2", "ITEM0022 x 1"];
    let summary = bestCharge(inputs).trim();
    let expected = `
============= 订餐明细 =============
黄焖鸡 x 1 = 18元
肉夹馍 x 2 = 12元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
指定菜品半价(黄焖鸡,凉皮),省13元
-----------------------------------
总计:25元
===================================`.trim();
    expect(summary).toEqual(expected);
  });

  it('should generate best charge when best is 满30减6元', function() {
    let inputs = ["ITEM0013 x 4", "ITEM0022 x 1"];
    let summary = bestCharge(inputs).trim();
    let expected = `
============= 订餐明细 =============
肉夹馍 x 4 = 24元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
满30减6元,省6元
-----------------------------------
总计:26元
===================================`.trim();
    expect(summary).toEqual(expected)
  });

  it('should generate best charge when no promotion can be used', function() {
    let inputs = ["ITEM0013 x 4"];
    let summary = bestCharge(inputs).trim();
    let expected = `
============= 订餐明细 =============
肉夹馍 x 4 = 24元
-----------------------------------
总计:24元
===================================`.trim()
    expect(summary).toEqual(expected)
  });

});