一、jasmine 简介
Jasmine就是一个行动驱动开发模式的JS的单元测试工具。
Suites(describe
)是Jasmine的核心,是一个测试集,里面包括多个specs(it
),而每个specs里面可能包含多个断言(expect
)。
- jasmine.js:整个框架的核心代码。
- jasmine-html.js:用来展示测试结果的js文件。
- boot.js:jasmine框架的的启动脚本。需要注意的是,这个脚本应该放在jasmine.js之后,自己的js测试代码之前加载。
- jasmine.css:用来美化测试结果。
在html页面引入以上文件,则可以进行jsamine测试
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 matcherexpect(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 matcherexpect(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 callsit("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 exceptionsdescribe("#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
语句表示,返回true
或false
。expect
语句有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
的函数名之前添加x
(xit
),那么该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 |
用于检测实际值是否为null 或undefined ,如果不为null 或undefined ,则返回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)
调用beforeEach
,it
或者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)});});