简介
Jasmine 是一款完全开源的基于行为驱动(BDD,behavior-driven development)的 JavaScript 测试框架,它不依赖于其他任何 JavaScript 组件,也不需要 DOM。它有灵活而清晰的语法,可以帮助我们最高效的完成测试代码。
不仅如此,对于 Jasmine 的测试结果,可以将直接现在在HTML
上。也可以将 Jasmine 与 持续集成系统(CI,Continuous Integration)对接,使得测试结果可以通过持续集成系统反馈给开发者。
所谓BDD(Behaviour Driven Development),是一种新的敏捷软件开发方法。
BDD与TDD(Test Driven Development)经常联系在一起。我的理解:
TDD
更多从技术层面阐述,是指当软件产生需求的时候,程序员应该先完成这一需求对应的测试用例(或者说用一系列测试用例来描述这一需求),再开始进行代码编码工作。最终以代码是否通过这一测试用例来判断代码与需求是否匹配。总的来说,TDD
更多是在描述码农的工作方式。
而BDD
更多是从完成业务需求的角度阐述,是指一种可降低开发者与非开发者(如项目经理)之间需求沟通和理解成本的工作方式,可使得非程序开发人员也能参与到编写测试用例的编写工作中来。其根本目的是希望码农开发出来的功能与项目模块的需求相切合。
Demo 体验
在 https://github.com/jasmine/jasmine/releases 中,官方有一个Demo,因此可以体验一把,并感觉一下其风格。
我下载的是jasmine-standalone-2.5.2
,对应地址:https://github.com/jasmine/jasmine/releases/download/v2.5.2/jasmine-standalone-2.5.2.zip。
解压后的文件结构:
// 运行测试用例的环境
SpecRunner.html
// 要测试的源代码
-src
// 测试用例
-spec
// jasmine的源代码
-lib
执行SpecRunner.html
以查看测试结果:
这里的测试用例,更像是业务人员描述的一个使用场景,且当这个使用场景发生时,期待的是什么状态。其实这就是对BDD
的诠释。
项目中添加 Jasmine
你可通过Node
,在项目中添加Jasmine
:
// Add Jasmine to your package.json
npm install --save-dev jasmine
// Initialize Jasmine in your project
jasmine init
// Run your tests
jasmine
当然,更常用的做法是,通过Gulp
or Grunt
构建工具以在项目以集成 Jasmine。
- Gulp: https://github.com/jasmine/gulp-jasmine-browser
- Grunt: https://github.com/gruntjs/grunt-contrib-jasmine
Jasmine 基本使用
- 官方API文档:https://jasmine.github.io/api/edge/global
- 官方介绍:https://jasmine.github.io/edge/introduction.html
以下例子均从官网搬运!
下来我们来详细讲解一下 Jasmine 中的一些概念,和测试用例的使用方法。
一个 Jasmine 例子:
// 对应一个Suite
describe('JavaScript addition operator',function(){
// 对应一个Spec
it('adds two numbers together',function(){
// 对应一个Expectation
expect(1 + 2).toEqual(3);
});
});
测试用例基本结构
Suites - 测试组
对应describe
函数块,通常包含一个到多个测试用例。
describe
函数包含两个参数:
- 第一个参数(String):通常描述被测试组件的名称,或一个测试的场景(where…,when…)
- 第二个参数(function):测试代码
Specs - 测试用例
对应it
函数块,通常仅仅包含一个测试用例。
it
函数包含两个参数:
- 第一个参数(String):通常描述期待的测试结果
- 第二个参数(function):测试代码
Expectations - 断言
对应expect
函数块,通常是一个具体的判断。
注意
注意,一个Suite(describe
)可以包含多个Suite(describe
),也可以包含多个Specs(it
)。一个Specs(it
)包含多个断言(expect
)。
字符串的描述
又一个 Jasmine 例子:
describe("Player", function() {
var player;
var song;
it("should be able to play a Song", function() {
player = player = new Player();
song = song = new Song();
player.play(song);
expect(player.currentlyPlayingSong).toEqual(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() {
player = player = new Player();
song = song = new Song();
player.play(song);
player.pause();
expect(player.isPlaying).toBeFalsy();
// demonstrates use of 'not' with a custom matcher
expect(player).not.toBePlaying(song);
});
});
});
在上面的例子中,把用例的描述连接起来,变成 it should be able to play a Song
、when song has been paused, it should indicate that the song is currently paused
。你会发现他们通常是一个完整的句子,而且是一个场景的描述。这就是BDD
的精髓所在。
Matches 判断
以下用例均可通过!
not
对于以下的每一个Match,都可以在前面加上not
,以表示非
。
比如,
describe("The 'toBe' matcher compares with ===", function() {
it("and has a positive case", function() {
expect(true).toBe(true);
});
it("and can have a negative case", function() {
expect(false).not.toBe(true);
});
});
toBe
相当于==
。
- 对于对象,比较引用是否相等
- 对于值,比较值是否相等
describe("The 'toBe' matcher", function() {
it("and so is a spec", function() {
var a = true;
expect(a).toBe(true);
});
it("The 'toBe' matcher compares with ===", function() {
var a = 12;
var b = a;
expect(a).toBe(b);
expect(a).not.toBe(null);
});
});
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);
});
});
toMatch
通常用toMatch配合正则表达式
来匹配字符串。
describe("The 'toMatch' matcher", function() {
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/);
});
});
toBeDefined与toBeUndefined
toBeDefined
是否已经被定义了。
describe("The 'toBeDefined' matcher",function() {
it("The 'toBeDefined' matcher compares against `undefined`",
function() {
var a = {
foo: "foo"
};
expect(a.foo).toBeDefined();
expect(a.bar).not.toBeDefined();
});
toBeUndefined
是否未被定义了。
describe("The 'toBeUndefined' matcher",function() {
it("The `toBeUndefined` matcher compares against `undefined`", function() {
var a = {
foo: "foo"
};
expect(a.foo).not.toBeUndefined();
expect(a.bar).toBeUndefined();
});
});
toBeTruthy 与 toBeFalsy
toBeTruthy
判断是否为 true。
describe("The 'toBeTruthy' matcher",function() {
it("The 'toBeTruthy' matcher is for boolean casting testing", function() {
var a, foo = "foo";
expect(foo).toBeTruthy();
expect(a).not.toBeTruthy();
});
});
toBeFalsy
判断是否为 false。
describe("The 'toBeFalsy' matcher",function() {
it("The 'toBeFalsy' matcher is for boolean casting testing", function() {
var a, foo = "foo";
expect(a).toBeFalsy();
expect(foo).not.toBeFalsy();
});
});
toContain
toContain
判断数组中是否包含某个元素。
describe("The 'toContain' matcher", function() {
it("works for finding an item in an Array", function() {
var a = ["foo", "bar", "baz"];
expect(a).toContain("bar");
expect(a).not.toContain("quux");
});
it("also works for finding a substring", function() {
var a = "foo bar baz";
expect(a).toContain("bar");
expect(a).not.toContain("quux");
});
});
toBeLessThan 和 toBeGreaterThan
toBeLessThan
判断是否小于,toBeGreaterThan
判断是否大于。
describe("The 'toBeLessThan' and 'toBeGreaterThan' matcher", function() {
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);
});
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);
});
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);
});
});
toThrow 和 toThrowError
toThrow
判断是否抛出了异常,toThrowError
判断是否抛出了特定的异常。
describe("The 'toThrow' and 'toThrowError' matcher",function() {
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;
};
var baz = function() {
throw 'what';
};
expect(foo).not.toThrow();
expect(bar).toThrow();
expect(baz).toThrow('what');
});
it("The 'toThrowError' matcher is for testing a specific thrown exception",function() {
var foo = function() {
throw new TypeError("foo bar baz");
};
expect(foo).toThrowError("foo bar baz");
expect(foo).toThrowError(/bar/);
expect(foo).toThrowError(TypeError);
expect(foo).toThrowError(TypeError, "foo bar baz");
});
});
一些操作
beforeEach 和 afterEach
为了增加测试用例模块的复用性,在一个测试用例集合中,对于一些执行测试用例前或执行测试用例后统一的操作可以放到beforeEach
函数和 afterEach
函数(或者beforeAll()
和afterAll()
)中。
如果你熟悉 Setup
和 Teardown
(来自于JUnit) ,其实他们和beforeEach
和 afterEach
就是一回事啦!
beforeEach()
:在describe
函数中的每个Spec
执行之前执行afterEach()
: 在describe
函数中每个Spec
数执行之后执行beforeAll()
:在describe
函数中所有的Specs
执行之前执行,在Sepc
之间不会被执行afterAll()
: 在describe
函数中所有的Specs
执行之后执行,在Sepc
之间不会被执行
还是举例子来说明beforeEach
和 afterEach
吧:
describe("A spec using beforeEach and afterEach", function() {
var foo = 0;
beforeEach(function() {
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() {
// 这个用例是通过的,是因为在上一个用例执行后,调用了afterEach
expect(foo).toEqual(1);
});
});
执行顺序:
- beforeEach
- it(“is just a function, so it can contain any code”)
- afterEach
- beforeEach
- it(“can have more than one expectation”)
- afterEach
再来举例子说明beforeAll()
和afterAll()
吧:
describe("A spec using beforeAll and afterAll", function() {
var foo;
beforeAll(function() {
foo = 1;
});
afterAll(function() {
foo = 0;
});
it("sets the initial value of foo before specs run", function() {
expect(foo).toEqual(1);
foo += 1;
});
it("does not reset foo between specs", function() {
expect(foo).toEqual(2);
});
});
执行顺序:
- beforeAll
- it(“sets the initial value of foo before specs run”)
- it(“does not reset foo between specs”)
- afterAll
总结
beforeAll()
和afterAll()
通常用于执行最初初始化和最终的清洁工作。再次强调,beforeAll()
和afterAll()
不会在每个Specs
之间执行,因此,每个Specs
之间的清洁工作需要放在beforeEach
和 afterEach
中。
手工使测试失败
在某些场合下,如果我们希望在满足某个条件时,测试用例失败,则可以使用fail
函数。
describe("A spec using the fail function",function() {
it("The 'toThrow' matcher is for testing if a function throws an exception",function() {
var a = true;
if(!a){
fail("Fail manually.");
}
expect(a).toBeTruthy();
});
});
在多个测试用例内部传值
this
关键字可用于在多个测试用例内部传值。
describe("A spec", function() {
beforeEach(function() {
this.foo = 0;
});
it("can use the `this` to share state", function() {
expect(this.foo).toEqual(0);
this.bar = "test pollution?";
});
it("prevents test pollution by having an empty `this` created for the next spec", function() {
expect(this.foo).toEqual(0);
expect(this.bar).toBe(undefined);
});
});
禁用测试用例
如果希望禁用某些测试用例,可以使用xdescribe
或xit
。
当被禁用后,这些 Suites 和 Specs 会被跳过,也不会在结果中出现。
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);
});
});
Jasmine 高阶使用
自定义的断言
除了上面介绍的系统定义的断言之外,也可以使用自己定义断言。
以 Jasmine 的 Demo 中的 toBePlaying
作为自定义断言的例子。
//demonstrates use of custom matcher
expect(player).toBePlaying(song);
定义:
beforeEach(function() {
jasmine.addMatchers({
toBePlaying: function() {
return {
compare: function(actual, expected) {
var player = actual;
return {
pass: player.currentlyPlayingSong === expected && player.isPlaying
};
}
};
}
});
});
高阶使用
测试函数被调用
spy
对象可用于跟踪和检查一个函数被调用的次数和传入的参数。
toHaveBeenCalled
:可以检查函数是否被调用过,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 that the spy was called x times", function() {
expect(foo.setBar).toHaveBeenCalledTimes(2);
});
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();
});
});
指定函数返回值
通过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);
foo.setBar(123);
fetchedBar = foo.getBar();
});
it("tracks that the spy was called", function() {
expect(foo.getBar).toHaveBeenCalled();
});
it("should not affect other functions", function() {
expect(bar).toEqual(123);
});
it("when called returns the requested value", function() {
expect(fetchedBar).toEqual(745);
});
});
调用函数时抛出指定异常
and.throwError
用于设置,当调用某个执行函数时,抛出指定异常。
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("quux");
});
it("throws the value", function() {
expect(function() {
foo.setBar(123)
}).toThrowError("quux");
});
});
构造一个mock对象
当没有一个具体的方法来spyon时,可以使用jasmine.createSpy
来创建一个mock对象。
describe("A spy, when created manually", function() {
var whatAmI;
beforeEach(function() {
whatAmI = jasmine.createSpy('whatAmI');
whatAmI("I", "am", "a", "spy");
});
it("is named, which helps in error reporting", function() {
expect(whatAmI.and.identity()).toEqual('whatAmI');
});
it("tracks that the spy was called", function() {
expect(whatAmI).toHaveBeenCalled();
});
it("tracks its number of calls", function() {
expect(whatAmI.calls.count()).toEqual(1);
});
it("tracks all the arguments of its calls", function() {
expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy");
});
it("allows access to the most recent call", function() {
expect(whatAmI.calls.mostRecent().args[0]).toEqual("I");
});
});
当然,还可以使用jasmine.createSpyObj
来创建一个mock对象,并且指定其包含特定的方法。
describe("Multiple spies, when created manually", function() {
var tape;
beforeEach(function() {
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
tape.play();
tape.pause();
tape.rewind(0);
});
it("creates spies for each requested function", function() {
expect(tape.play).toBeDefined();
expect(tape.pause).toBeDefined();
expect(tape.stop).toBeDefined();
expect(tape.rewind).toBeDefined();
});
it("tracks that the spies were called", function() {
expect(tape.play).toHaveBeenCalled();
expect(tape.pause).toHaveBeenCalled();
expect(tape.rewind).toHaveBeenCalled();
expect(tape.stop).not.toHaveBeenCalled();
});
it("tracks all the arguments of its calls", function() {
expect(tape.rewind).toHaveBeenCalledWith(0);
});
});
mock 当前时间
比如一些操作,是经过多少时间后、或者间隔多少时间才会触发的,可以mock当前时间。
注意,需要在开始前调用 jasmine.clock().install
,在结束时调用jasmine.clock().uninstall()
。
jasmine.clock().tick
用于指定经过了多长时间。
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);
expect(timerCallback).not.toHaveBeenCalled();
jasmine.clock().tick(101);
expect(timerCallback).toHaveBeenCalled();
});
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);
jasmine.clock().tick(50);
expect(timerCallback.calls.count()).toEqual(2);
});
describe("Mocking the Date object", function(){
it("mocks the Date object and sets it to a given time", function() {
var baseTime = new Date(2013, 9, 23);
jasmine.clock().mockDate(baseTime);
jasmine.clock().tick(50);
expect(new Date().getTime()).toEqual(baseTime.getTime() + 50);
});
});
});
Jasmine 与其他工具集成
Jasmine 默认不包括任何测试执行管理工具(当然这不意味着你不可以通过打开SpecRunner.html
来执行所有测试)。
因此,使用 Jasmine 的最佳实践是以 Karma 作为 JavaScript 测试执行过程管理工具。
换句话说,如果你不使用任何自动化构建框架,你可以直接手动的打开SpecRunner.html
来执行所有测试用例。但是,在当今的敏捷开发时代,尽可能将大部分流程自动化,往往是更科学更高效的开发流程,这时候我们就要使用 Karma 了。
这个关系好比,在 Java 中, JUnit 做单元测试, Maven 以使 JUnit 单元测试自动化进行。
关于 Karma 的安装:https://github.com/karma-runner/karma-jasmine
参考
- Wikipedia - Jasmine (JavaScript testing framework)
- https://jasmine.github.io/
- JavaScript 单元测试框架:Jasmine 初探
- jasmine行为驱动,测试先行
- Jasmine 基础使用教程