开JavaScript测试的路–Jasmine

初识Jasmine

<small><small>Jin Sun, January 17,
2016
</small></small>

咱俩若权来什么:

  1. 一个是的序曲
  2. 简单易行粗暴的牵线
  3. 那我们开吧
  4. 本身该如何以与否
  5. 暨KnockoutJS不得不说的故事

引子

咱不欠解决问题之能力,我们少的唯有是重复早发现题目之方式,而自动化测试可协助我们,所以吃我们迎接
<big><big>Jasmine</big></big>.

介绍

Jasmine 是一款 JavaScript 测试框架,它不负让其它任何 JavaScript
组件。它发清清晰的告诉如法炮制,让您可非常简单的描绘起测试代码。

开始

  1. 前往 Jasmine
    官网下载standalone版本。

    图片 1
    image

  2. 将jasmine-standalone-xxx.zip解压,运行SpecRunner.html,你会见到底的界面:

![]()
image
  1. 开辟SpecRunner.html,我们看它的用法:

<html>
<head>
    <meta charset="utf-8">
    <title>Jasmine Spec Runner v2.4.1</title>

    <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.4.1/jasmine_favicon.png">
    <link rel="stylesheet" href="lib/jasmine-2.4.1/jasmine.css">
    <!-- 测试界面css样式 -->
    <script src="lib/jasmine-2.4.1/jasmine.js"></script>
    <!-- 核心文件用于执行单元测试的类库 -->
    <script src="lib/jasmine-2.4.1/jasmine-html.js"></script>
    <!-- 用于显示单元测试结果的类库 -->
    <script src="lib/jasmine-2.4.1/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>

使用

打开测试文件PlayerSpec.js,我们见面视describe,it,beforeEach,afterEach,expect,toBe….都是数什么意思呢?

Jasmine有四只基本概念:分组(Suites)用例(Specs)期望(Expectations)匹配(Matchers).

Suites

Suites可以清楚为同组测试用例,使用全局的Jasmin函数describe
创建。describe函数接受两只参数,一个字符串和一个函数。字符串是是Suites的名字或标题(通常描述下测试内容),函数是落实Suites的代码块。

Specs

Specs可以清楚吧一个测试用例,使用全局的Jasmin函数it创建。和describe一样承受两独参数,一个字符串和一个函数,函数就是设尽之测试代码,字符串就是测试用例的名字。一个Spec可以分包多只expectations来测试代码。

Expectations

Expectations由expect
函数创建。接受一个参数。和Matcher一起联用,设置测试的预期值。

于分组(describe)中得以写多个测试用例(it),也足以再次进行分组(describe),在测试用例(it)中定义期望表达式(expect)和相当判断(toBe**)。看一个简单易行的Demo:

describe("A suite", function() {//suites
    var a;
    it("A spec", function() {//spec
      a = true;
      expect(a).toBe(true);//expectations
    });

    describe("a suite", function() {//inner suites
           it("a spec", function() {//spec
           expect(a).toBe(true);//expectations
        });
  });
});

Matchers

Matcher实现一个“期望值”与“实际值”的自查自纠,如果结果也true,则通过测试,反之,则败。每一个matcher都能经过not执行否定判断。

简单的matchers:

expect(a).toBe(true);//期望变量a为true
expect(a).toEqual(true);//期望变量a等于true
expect(a).toMatch(/reg/);//期望变量a匹配reg正则表达式,也可以是字符串
expect(a.foo).toBeDefined();//期望a.foo已定义
expect(a.foo).toBeUndefined();//期望a.foo未定义
expect(a).toBeNull();//期望变量a为null
expect(a.isMale).toBeTruthy();//期望a.isMale为真
expect(a.isMale).toBeFalsy();//期望a.isMale为假
expect(true).toEqual(true);//期望true等于true
expect(a).toBeLessThan(b);//期望a小于b
expect(a).toBeGreaterThan(b);//期望a大于b
expect(a).toThrowError(/reg/);//期望a方法抛出异常,异常信息可以是字符串、正则表达式、错误类型以及错误类型和错误信息
expect(a).toThrow();//期望a方法抛出异常
expect(a).toContain(b);//期望a(数组或者对象)包含b

其他matchers:

jasmine.any(Class)–传入构造函数或者类归数据类型作为想值,返回true表示实际值和期望值数据类型相同:

it("matches any value", function() {
    expect({}).toEqual(jasmine.any(Object));
    expect(12).toEqual(jasmine.any(Number));
});

jasmine.anything()–如果实在价值不是null或者undefined则赶回true:

it("matches anything", function() {
    expect(1).toEqual(jasmine.anything());
});

jasmine.objectContaining({key:value})–实际数组只要匹配到发出隐含的数值便匹配通过:

foo = {
      a: 1,
      b: 2,
      bar: "baz"
};
expect(foo).toEqual(jasmine.objectContaining({bar: "baz"}));

jasmine.arrayContaining([val1,val2,…])–stringContaining可以匹配配字符串的一模一样有为堪配合对象内的字符串:

expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)});
expect('foobarbaz').toEqual({foo: jasmine.stringMatching('bar')});

<small>Jasmine还支持由定义Matchers,今天咱们先行不开展了.</small>

Setup and Teardown

为在纷繁的测试用例中益方便组装和拆卸,Jasmine提供了季个函数:

beforeEach(function)  //在每一个测试用例(it)执行之前都执行一遍beforeEach函数;
afterEach(function)  //在每一个测试用例(it)执行完成之后都执行一遍afterEach函数;
beforeAll(function)  //在所有测试用例执行之前执行一遍beforeAll函数;
afterAll(function)  //在所有测试用例执行完成之后执行一遍afterAll函数;

相比之下一下结果看一下底下的例子就是一目了然啦:

describe("A spec using beforeEach and afterEach", function() {
  var foo = 0;
  beforeEach(function() {
    foo += 1;

    le.log("I am beforEach");
  });
  afterEach(function() {
    foo = 0;
    console.log("I am afterEach");
  });
  it("A spec1", function() {
    expect(foo).toEqual(1);
    console.log("I am spec1");
  });
  it("A spec2", function() {
    expect(foo).toEqual(1);
    expect(true).toEqual(true);
    console.log("I am spec2");
  });
});
describe("A spec using beforeAll and afterAll", function() {
  var foo;
  beforeAll(function() {
    foo = 1;
    console.log("I am beforAll");
  });
  afterAll(function() {
    foo = 0;
    console.log("I am afterAll");
  });
  it("A spec1", function() {
    expect(foo).toEqual(1);
    foo += 1;
    console.log("I am A spec1");
  });
  it("A spec2", function() {
    expect(foo).toEqual(2);
    console.log("I am A spec2");
  });
});

说到底输出:

图片 2

image

Suites禁用和Specs挂起

Jasmine提供xdescrib和xit方法用于屏蔽测试用例,xdescribe里面的定义的it、beforeEach、afterEach等之类的不二法门无会见履行,describe里面定义的xit不见面实施。
测试文档遇到用例挂于底地方便未会见尽要的匹配表达式,有三种下办法:

describe("Pending specs", function() {
  xit("can be declared 'xit'", function() {//第一种,使用xit将测试用例直接屏蔽
    expect(true).toBe(false);
  });
 it("can be declared with 'it' but without a function");//第二种,只声明it,不定义回调函数
 it("can be declared by calling 'pending' in the spec body", function() {
    expect(true).toBe(false);
    pending('this is why it is pending');//第三种,使用pending函数
  });
});

Spy追踪

Jasmine有函数的寻踪和倒追踪的双重效益,这东西就是Spy!
Spy能够存储任何函数调用记录以及传播的参数,Spy只设有于describe同it中,在spec执行了之后销毁。说的如此隐晦,还是一直上例子吧:

describe("A spy", function() {
  var foo, bar = null;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };
    spyOn(foo, 'setBar');//给foo对象的setBar函数绑定追踪
    foo.setBar(123);
    foo.setBar(456, 'another param');
  });
  it("tracks that the spy was called", function() {
    expect(foo.setBar).toHaveBeenCalled();//toHaveBeenCalled用来匹配测试函数是否被调用过
  });
  it("tracks all the arguments of its calls", function() {
    expect(foo.setBar).toHaveBeenCalledWith(123);//toHaveBeenCalledWith用来匹配测试函数被调用时的参数列表
    expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');//期望foo.setBar已经被调用过,且传入参数为[456, 'another param']
  });
  it("stops all execution on a function", function() {
    expect(bar).toBeNull();//用例没有执行foo.setBar,bar为null
  });
});

and.callThrough–spy链式调用and.callThrough后,在赢得spy的以,调用实际的函数,看示例:

describe("A spy, when configured to call through", function() {
  var foo, bar, fetchedBar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };
    spyOn(foo, 'getBar').and.callThrough();//调用and.callThrough方法
    foo.setBar(123);
    fetchedBar = foo.getBar();//因为and.callThrough,这里执行的是foo.getBar方法,而不是spy的方法
  });
  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.returnValue–spy链式调用and.returnValue
后,任何时刻调用该法都止见面回指定的值,比如:

describe("A spy, when configured to fake a return value", function() {
  var foo, bar, fetchedBar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };
    spyOn(foo, "getBar").and.returnValue(745);//指定返回值为745
    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(745);//默认返回指定的returnValue值
  });
});

and.callFake–spy链式添加and.callFake相当给用新的计替换spy的措施,比如:

describe("A spy, when configured with an alternate implementation", function() {
  var foo, bar, fetchedBar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };
    spyOn(foo, "getBar").and.callFake(function() {//指定callFake方法
      return 1001;
    });
    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(1001);//执行callFake方法,返回1001
  });
});

and.throwError–spy链式调用and.callError后,任何时刻调用该措施都见面废弃来很错误信息:

describe("A spy, when configured to throw an error", function() {
  var foo, bar;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };
    spyOn(foo, "setBar").and.throwError("error");//指定throwError
  });
  it("throws the value", function() {
    expect(function() {
      foo.setBar(123)
    }).toThrowError("error");//抛出错误异常
  });
});

and.stub–spy恢复至老状态,不履另外操作。直接扣下代码:

describe("A spy", function() {
  var foo, bar = null;
  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };
    spyOn(foo, 'setBar').and.callThrough();
  });
  it("can call through and then stub in the same spec", function() {
    foo.setBar(123);
    expect(bar).toEqual(123);
    foo.setBar.and.stub();//把foo.setBar设置为原始状态,and.callThrough无效
    bar = null;
    foo.setBar(123);//执行赋值无效
    expect(bar).toBe(null);
  });
});
Spy的另方式
.calls.any():记录spy是否被访问过,如果没有,则返回false,否则,返回true;
.calls.count():记录spy被访问过的次数;
.calls.argsFor(index):返回指定索引的参数;
.calls.allArgs():返回所有函数调用的参数记录数组;
.calls.all ():返回所有函数调用的上下文、参数和返回值;
.calls.mostRecent():返回最近一次函数调用的上下文、参数和返回值;
.calls.first():返回第一次函数调用的上下文、参数和返回值;
.calls.reset():清除spy的所有调用记录;

编造定时器

Jasmine Clock 使用setTimeout 和setInterval
来声称定时的回调操作。它而回调函数同步执行,当Clock的tick时间跨timer的年华,回调函数会为触发一蹩脚。
Jasmine Clock以jasmine.clock().install
以用调用timer函数的spec和suite中初始化。在尽了测试的下,一定要是卸载Clock来还原timer函数。使用jasmine.clock().tick来促进时为要注册的回调触发。
Install–在Spec或者Suite中安装Jasmine Clock:

beforeEach(function() {
    timerCallback = jasmine.createSpy("timerCallback");
    jasmine.clock().install();
});

Uninstall–保证使用到位后,切记要关闭Jasmine Clock:

afterEach(function() {
    jasmine.clock().uninstall();
});

Tick–以jasmine.clock().tick来算时,一旦累计的光阴及setTimeout或者setInterval中指定的延时时间,则触发回调函数:

describe("Manually ticking the Jasmine Clock", function() {
  var timerCallback;
  beforeEach(function() {
    timerCallback = jasmine.createSpy("timerCallback");
    jasmine.clock().install();
  });
  afterEach(function() {
    jasmine.clock().uninstall();
  });
  it("causes a timeout to be called synchronously", function() {
    setTimeout(function() {
      timerCallback();
    }, 100);//声明回调函数tick到100ms就触发

    expect(timerCallback).not.toHaveBeenCalled();


    jasmine.clock().tick(101);//tick 101 会触发上面注册的setTimeout

    expect(timerCallback).toHaveBeenCalled();
  });
});

Mock Date

describe("Mocking the Date object", function(){
    it("mocks the Date object and sets it to a given time", function() {
      var baseTime = new Date(2016, 1, 27);//new一个指定的时间,没有参数则返回当前时间
        jasmine.clock().mockDate(baseTime);//构造一个虚拟的当前时间
        jasmine.clock().tick(50);//让虚拟的当前时间快进50ms
        expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
    });
  });
});

异步支持

Jasmine支持测试需要实行异步操作的specs,调用beforeEach , it ,
和afterEach 的时节,可以带来一个可选的参数done
,当spec执行到位之后要调用done 来报Jasmine异步操作就完成。
默认Jasmine的过期时间是5s,可以经过全局的jasmine.DEFAULTTIMEOUTINTERVAL
设置。

describe("Asynchronous specs", function() {
  var value;
  beforeEach(function(done) {//传入done参数表示要执行异步操作
    setTimeout(function() {
      value = 0;
      done();//执行done()函数通知it异步操作已经执行完毕,必须执行
    }, 10000);
  });
 it("should support async execution of test preparation and expectations", function(done) {//传入done参数表示要执行异步操作
    value++;
    expect(value).toBeGreaterThan(0);
    done();//执行done()函数通知it异步操作已经执行完毕,必须执行
  });
});

Ajax

Jasmine拥有一个用以测试Ajax请求的plug-in:

describe("mocking ajax", function() {
  describe("suite wide usage", function() {
    beforeEach(function() {
      jasmine.Ajax.install();
    });
    afterEach(function() {
      jasmine.Ajax.uninstall();
    });

    it("specifying response when you need it", function() {
      var doneFn = jasmine.createSpy("success");
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function(args) {
        if (this.readyState == this.DONE) {
          doneFn(this.responseText);
        }
      };

      xhr.open("GET", "/some/cool/url");
      xhr.send();
      expect(jasmine.Ajax.requests.mostRecent().url).toBe('/some/cool/url');
      expect(doneFn).not.toHaveBeenCalled();
      jasmine.Ajax.requests.mostRecent().response({
        "status": 200,
        "contentType": 'text/plain',
        "responseText": 'awesome response'
      });
      expect(doneFn).toHaveBeenCalledWith('awesome response');
    });
    it("allows responses to be setup ahead of time", function () {
      var doneFn = jasmine.createSpy("success");
      jasmine.Ajax.stubRequest('/another/url').andReturn({
        "responseText": 'immediate response'
      });
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function(args) {
        if (this.readyState == this.DONE) {
          doneFn(this.responseText);
        }
      };

      xhr.open("GET", "/another/url");
      xhr.send();

      expect(doneFn).toHaveBeenCalledWith('immediate response');
    });
  });
  it("allows use in a single spec", function() {
    var doneFn = jasmine.createSpy('success');
    jasmine.Ajax.withMock(function() {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function(args) {
        if (this.readyState == this.DONE) {
          doneFn(this.responseText);
        }
      };

      xhr.open("GET", "/some/cool/url");
      xhr.send();

      expect(doneFn).not.toHaveBeenCalled();

      jasmine.Ajax.requests.mostRecent().response({
        "status": 200,
        "responseText": 'in spec response'
      });

      expect(doneFn).toHaveBeenCalledWith('in spec response');
    });
  });
});

KnockoutJS

What should and shouldn’t be tested?

用人不疑,疑人不用。所以您切莫需测试Knockout正确的拿一个ko.observable()对象出示在一个前台data-bind="text: ..."的span对象上,同样我们为非需测试ko.computed会面以外借助发生变化时运行。那么我们测试什么啊?答案就是逻辑,我们相应还多的体贴自己所勾画的逻辑代码,这些才是测试的重要性。

一个简约栗子

var addressBookViewModel = {
  entries : ko.observableArray([]),
  newEntryFirstName : ko.observable(),
  newEntrySurname : ko.observable()
    addNewEntry : function() {
        var newEntry = {
          firstName : this.newEntryFirstName(),
          surname : this.newEntrySurname()
      };
      this.entries.push(newEntry);
      // clear form
      this.newEntryFirstName('');
      this.newEntrySurname('');
  }
};

ko.applyBindings(addressBookViewModel);
分析

好想像一下此ViewModel的以场景,用户输入firstname,一个surname以点击绑定了’addNewEntry’的按钮。分析逻辑我们发现,此时一个entry给投入了entries(可能会见因此在一个table里通过foreach展示),然后清空。
在开始测试之前我们先是使指向代码进行部分重构,因为首先这个ViewModel大凡单例的,并且当前艺术验证点太多我们得拆分一下。重构后底代码如下:

function AddressBookViewModel() {
  this.entries = ko.observableArray([]);
  this.newEntryFirstName = ko.observable();
  this.newEntrySurname = ko.observable();
  this.addNewEntry = function() {
      addAddressBookEntry(this.newEntryFirstName, this.newEntrySurname, this.entries);
      clearObservables([this.newEntryFirstName, this.newEntrySurname]);
  };
}

function addAddressBookEntry(firstName, surname, list) {
  var newEntry = {
      firstName : ko.toJS(firstName),
      surname : ko.toJS(surname)
  };
  list.push(newEntry);
}

function clearObservables(observables) {
  observables.forEach(function(observable){
      observable(null);
  });
}

var addressBookViewModel = new AddressBookViewModel();
ko.applyBindings(addressBookViewModel);
初步测试
  • addAddressBookEntry

// 'Describe' creates a Jasmine test. A describe block contains assertions, using the 'it' function.
describe('addAddressBookEntry', function(){

  var newEntryFirstName, newEntryLastName, list;

  // 'beforeEach' performs setup before each 'it' test
  beforeEach(function(){
      newEntryFirstName = ko.observable('Peggy');
      newEntryLastName = ko.observable('Hill');
      list = ko.observableArray([]);
  });

  it('Adds an entry to the provided list', function(){
      var initialListLength = list().length;
      addAddressBookEntry(newEntryFirstName, newEntryLastName, list);
      var newListLength = list().length;

      // Jasmine uses the 'expect' function for assertions. Its format is very human-readable.
      // If an expection proves false, it will throw an exception and the assertion will be reported as failed.
      expect(newListLength).toBe(initialListLength + 1);
  });

  it('Adds an entry containing the supplied firstname and surname', function(){
      addAddressBookEntry(newEntryFirstName, newEntryLastName, list);
      var unwrappedList = list();
      var expectedNewEntry = {firstName: 'Peggy', surname: 'Hill'};
      // Jasmine's toContain will, amongst other things, test whether an array contains an object with fields matching a supplied object
      expect(unwrappedList).toContain(expectedNewEntry);
  });

  it('Adds the entry to the end of the list', function(){
      addAddressBookEntry(newEntryFirstName, newEntryLastName, list);
      var unwrappedList = list();
      var lastEntry = unwrappedList[unwrappedList.length - 1];

      // You can have multiple expectations in a Jasmine test
      expect(lastEntry.firstName).toBe('Peggy');
      expect(lastEntry.surname).toBe('Hill');
  });

});

一个普普通通栗子

function FormatterBinding(formatter) {
  this.update = function update(element, valueAccessor) {
      var newModelValue = ko.unwrap(valueAccessor());
      var formattedText = formatter(newModelValue);
      // let's assume we don't need to support IE8
      element.textContent = formattedText;
  };
}

function formatNumberAsDollars(number) {
  return number.toLocaleString('en-US',{style: 'currency', currency: 'USD', maximumFractionDigits: 2});
}
分析

这个FormatterBinding是用来拿数字格式化为货币。对于下边的formatNumberAsDollars我们不需要测试,我们只是待关注FormatterBinding

var mockFormatter, customBinding, mockValueAccessor;
// 'beforeEach' performs setup before each 'it' test
beforeEach(function(){
  mockFormatter = jasmine.createSpy('mockFormatter');
  customBinding = new FormatterBinding(mockFormatter);
  mockElement = document.createElement('p');
  mockValueAccessor = function() {
      return ko.observable('someMockValue');
  }
});

it('Calls the formatter with the value in the valueAccessor', function(){
  // let's assume we've created all our mocks as part of a Jasmine
  // beforeEach block that runs before each set of assertions

  customBinding.update(mockElement, mockValueAccessor);
  expect(mockFormatter).toHaveBeenCalledWith('someMockValue');
});

it('Prints the output of the formatter to the element', function(){
  // Setting up the spy is a little bit awkward
  var mockFunctions = {
      mockFormatter : function() { return 'I am the mockFormatter return value';}
  };
  spyOn(mockFunctons, 'mockFormatter');

  // But everything else is dead easy
  var customBinding = new FormatterBinding(mockFunctions.mockFormatter);
  customBinding.update(mockElement, mockValueAccessor);
  var mockElementContent = mockElement.textContent;
  expect(mockElementContent).toBe('I am the mockFormatter return value');
});

只是并无时间再去准备高级栗子了…

相关文章