JavaScript单元测试框架-Jasmine
Jasmine的开发团队来自PivotalLabs,他们一开始开发的JavaScript测试框架是JsUnit,来源于著名的JAVA测试框架JUnit。JsUnit是xUnit的JavaScript实现。但是JsUnit在2009年后就已经停止维护了,他们推出了一个新的BDD框架Jasmine。Jasmine不依赖于任何框架,所以适用于所有的Javascript代码。
所谓BDD(行为驱动开发,Behaviour Driven Development),是一种新的敏捷开发方法。Dan North对BDD给出的定义为:
BDD是第二代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,可以具有带有良好定义的输出(即工作中交付的结果):已测试过的软件。
BDD与TDD(Test Driven Development )的主要区别是,使得非程序人员也能参与到测试用例的编写中来,大大降低了客户、用户、项目管理者与开发者之间来回翻译的成本。所以BDD更加注重业务需求而不是技术[1]。
下载
在Jasmine的Github官方主页:https://github.com/jasmine/jasmine
找到上方的releases,点击会跳转到https://github.com/jasmine/jasmine/releases。
下载已发布的zip包,比如下载当前(2015-03-09)的最新版本为:jasmine-standalone-2.2.0.zip
目录结构
解压之后,可以看到有1个html文件和3个文件夹。
- lib:存放了运行测试案例所必须的文件,其内包含
jasmine-2.2.0
文件夹。可以将不同版本的Jasmine放在lib下,以便使用时切换。- jasmine.js:整个框架的核心代码。
- jasmine-html.js:用来展示测试结果的js文件。
- boot.js:jasmine框架的的启动脚本。需要注意的是,这个脚本应该放在jasmine.js之后,自己的js测试代码之前加载。
- jasmine.css:用来美化测试结果。
- spec:存放测试脚本。
- PlayerSpec.js:就是针对src文件夹下的Player.js所写的测试用例。
- SpecHelper.js:用来添加自定义的检验规则,如果框架本身提供的规则(诸如
toBe
,toNotBe
等)不适用,就可以额外添加自己的规则(在本文件中添加了自定义的规则toBePlaying
)。
- src:存放需要测试的js文件。Jasmine提供了一个Example(Player.js,Song.js)。
- SpecRunner.html:运行测试用例的环境。它将上面3个文件夹中一些必要的文件都包含了进来。如果你想将自己的测试添加进来的话,那么就修改相应的路径。
其中,spec文件夹,src文件夹和SpecRunner.html文件是Jasmine提供的一个完整示例,用浏览器打开 SpecRunner.html,即可看到执行的结果。
SpecRunner.html
运行测试用例的例子:
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.2.0</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.2.0/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.2.0/jasmine.css">
<script src="lib/jasmine-2.2.0/jasmine.js"></script>
<script src="lib/jasmine-2.2.0/jasmine-html.js"></script>
<script src="lib/jasmine-2.2.0/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>
核心概念
框架中的一些核心概念,可以参考官方文档中的介绍[2]。下面进入搬砖模式:
Suites
Suite表示一个测试集,以函数describe(string, function)
封装,它包含2个参数:
string
:测试组名称,
function
:测试组函数。
一个Suite(describe
)包含多个Specs(it
),一个Specs(it
)包含多个断言(expect
)。
Setup和Teardown操作
Jasmine的Setup和Teardown操作(Setup在每个测试用例Spec执行之前做一些初始化操作,Teardown在每个Sepc执行完之后做一些清理操作,这两个函数名称来自于JUnit),是由一组全局beforeEach
,afterEach
, beforeAll
,afterAll
函数来实现的。
- beforeEach():在
describe
函数中每个Spec执行之前执行。 - afterEach(): 在
describe
函数中每个Spec数执行之后执行。 - beforeAll():在
describe
函数中所有的Specs执行之前执行,但只执行一次,在Sepc之间并不会被执行。 - afterAll(): 在
describe
函数中所有的Specs执行之后执行,但只执行一次,在Sepc之间并不会被执行。
beforeAll
和 afterAll
适用于执行比较耗时或者耗资源的一些共同的初始化和清理工作。而且在使用时还要注意,它们不会在每个Spec之间执行,所以不适用于每次执行前都需要干净环境的Spec。
this值
除了在describe
函数开始定义变量,用于各it
函数共享数据外,还可以通过this
关键字来共享数据。
在在每一个Spec的生命周期(beforeEach->it->afterEach
)的开始,都将有一个空的this
对象(在开始下一个Spec周期时,this
会被重置为空对象)。
嵌套Suite
describe
函数可以嵌套,每层都可以定义Specs。这样就可以让一个Suite由一组树状的方法组成。
每个嵌套的describe
函数,都可以有自己的beforeEach
,afterEach
函数。
在执行每个内层Spec时,都会按嵌套的由外及内的顺序执行每个beforeEach
函数,所以内层Sepc可以访问到外层Sepc中的beforeEach
中的数据。类似的,当内层Spec执行完成后,会按由内及外的顺序执行每个afterEach
函数。
describe("A spec", function() {
var foo;
beforeEach(function() {
foo = 0;
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() {
expect(foo).toEqual(1);
expect(true).toEqual(true);
});
describe("nested inside a second describe", function() {
var bar;
beforeEach(function() {
bar = 1;
});
it("can reference both scopes as needed", function() {
expect(foo).toEqual(bar);
});
});
});
Specs
Spec表示测试用例,以it(string, function)
函数封装,它也包含2个参数:
string
:测试用例名称,
function
:测试用例函数。
Expectations
Expectation就是一个断言,以expect
语句表示,返回true
或false
。expect
语句有1个参数,代表要测试的实际值(the actual)。
只有当一个Spec中的所有Expectations全为ture
时,这个Spec才通过,否则失败。
Expectation带实际值,它和表示匹配规则的Matcher链接在一起,Matcher带有期望值。
Matchers
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);
});
- toHaveBeenCalled()
- toHaveBeenCalledWith()
- toMatch():按正则表达式匹配。
- toNotMatch()
- toThrow():检验一个函数是否会抛出一个错误
自定义Matchers的实现
自定义Matcher(被称为Matcher Factories)实质上是一个函数(该函数的参数可以为空),该函数返回一个闭包,该闭包的本质是一个compare
函数,compare
函数接受2个参数:actual value 和 expected value。
compare
函数必须返回一个带pass
属性的结果Object,pass
属性是一个Boolean值,表示该Matcher的结果(为true
表示该Matcher实际值与预期值匹配,为false
表示不匹配),也就是说,实际值与预期值具体的比较操作的结果,存放于pass
属性中。
最后测试输出的失败信息应该在返回结果Object中的message
属性中来定义。
var customMatchers = {
toBeGoofy: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
if (expected === undefined) {
expected = '';
}
var result = {};
result.pass = util.equals(actual.hyuk, "gawrsh" + expected, customEqualityTesters);
if (result.pass) {
result.message = "Expected " + actual + " not to be quite so goofy";
} else {
result.message = "Expected " + actual + " to be goofy, but it was not very goofy";
}
return result;
}
};
}
};
自定义Matchers的使用
对自定义Matcher有2种使用方法:
- 将该函数添加到一个特定的
describe
函数的beforeEach
中,以便该describe
函数中的所有Spec都能调用到它。但其他describe
中并不能使用该Matcher。
该方法的例子可以参考官网提供的custom_matcher.js
的实现[3]。
describe("Custom matcher: 'toBeGoofy'", function() {
beforeEach(function() {
jasmine.addMatchers(customMatchers);
});
it("can take an 'expected' parameter", function() {
expect({
hyuk: 'gawrsh is fun'
}).toBeGoofy(' is fun');
});
});
- 将该函数添加到全局的
beforeEach
函数中,这样所有的Suites中的所有的Specs,都可以使用该Matcher。
该方法的例子可以参考Jasmine提供的Demo中的SpecHelper.js
文件中的toBePlaying
自定义的规则的实现。
//定义
beforeEach(function () {
jasmine.addMatchers({
toBePlaying: function () {
// 自定义Matcher:toBePlaying
return {
//要返回的compare函数
compare: function (actual, expected) {
var player = actual;
//compare函数中要返回的结果Object,这里是一个匿名Object,包含一个pass属性。
return {
pass: player.currentlyPlayingSong === expected && player.isPlaying
}
}
};
}
});
});
//使用
describe("Player", function() {
it("should be able to play a Song", function() {
player.play(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() {
// demonstrates use of 'not' with a custom matcher
expect(player).not.toBePlaying(song);
});
)};
禁用Suites
Suites可以被Disabled。在describe
函数名之前添加x
即可将Suite禁用。
被Disabled的Suites在执行中会被跳过,该Suite的结果也不会显示在结果集中。
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);
});
});
挂起Specs
有3种方法可以将一个Spec标记为Pending。被Pending的Spec不会被执行,但是Spec的名字会在结果集中显示,只是标记为Pending。
- 如果在Spec函数
it
的函数名之前添加x
(xit
),那么该Spec就会被标记为Pending。 - 一个没有定义函数体的Sepc也会在结果集中被标记为Pending。
- 如果在Spec的函数体中调用
pending()
函数,那么该Spec也会被标记为Pending。pending()
函数接受一个字符串参数,该参数会在结果集中显示在PENDING WITH MESSAGE:
之后,作为为何被Pending的原因。
describe("Pending specs", function() {
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');
});
});
高级特性
Spy
Spy能监测任何function的调用和方法参数的调用痕迹。需使用2个特殊的Matcher:
toHaveBeenCalled
:可以检查function是否被调用过,toHaveBeenCalledWith
: 可以检查传入参数是否被作为参数调用过。
spyOn
使用spyOn(obj,'function')
来为obj
的function
方法声明一个Spy。不过要注意的一点是,对Spy函数的调用并不会影响真实的值。
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() {
// Spy的调用并不会影响真实的值,所以bar仍然是null。
expect(bar).toBeNull();
});
});
and.callThrough
如果在spyOn
之后链式调用and.callThrough
,那么Spy除了跟踪所有的函数调用外,还会直接调用函数额真实实现,因此Spy返回的值就是函数调用后实际的值了。
...
spyOn(foo, 'getBar').and.callThrough();
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(123);
});
});
and.stub
在调用and.callThrough后,如果你想阻止spi继续对实际值产生影响,你可以调用and.stub。也就是说,and.stub是将spi对实际实现的影响还原到最终的状态——不影响实际值。
spyOn(foo, 'setBar').and.callThrough();
foo.setBar(123);
// 实际的bar=123
expect(bar).toEqual(123);
// 调用and.stub()后,之后调用foo.setBar将不会影响bar的值。
foo.setBar.and.stub();
foo.setBar(456);
expect(bar).toBe(123);
bar = null;
foo.setBar(123);
expect(bar).toBe(null);
全局匹配谓词
jasmine.any
jasmine.any
的参数为一个构造函数,用于检测该参数是否与实际值所对应的构造函数相匹配。
describe("jasmine.any", function() {
it("matches any value", function() {
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
});
describe("when used with a spy", function() {
it("is useful for comparing arguments", function() {
var foo = jasmine.createSpy('foo');
foo(12, function() {
return true;
});
expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
});
});
});
jasmine.anything
jasmine.anything
用于检测实际值是否为null
或undefined
,如果不为null
或undefined
,则返回true
。
it("matches anything", function() {
expect(1).toEqual(jasmine.anything());
});
jasmine.objectContaining
用于检测实际Object值中是否存在特定key/value对。
var foo;
beforeEach(function() {
foo = {
a: 1,
b: 2,
bar: "baz"
};
});
it("matches objects with the expect key/value pairs", function() {
expect(foo).toEqual(jasmine.objectContaining({
bar: "baz"
}));
expect(foo).not.toEqual(jasmine.objectContaining({
c: 37
}));
});
jasmine.arrayContaining
用于检测实际Array值中是否存在特定值。
var foo;
beforeEach(function() {
foo = [1, 2, 3, 4];
});
it("matches arrays with some of the values", function() {
expect(foo).toEqual(jasmine.arrayContaining([3, 1]));
expect(foo).not.toEqual(jasmine.arrayContaining([6]));
});
Jasmine Clock
Jasmine Clock用于setTimeout
和setInterval
的回调控制,它使timer
的回调函数同步化,不再依赖于具体的时间,而是将时间离散化,使测试人员能精确控制具体的时间点。
安装与卸载
调用jasmine.clock().install()
可以在特定的需要操纵时间的Spec或者Suite中安装Jasmine Clock,注意操作完后要调用jasmine.clock().uninstall()
进行卸载。
var timerCallback;
beforeEach(function() {
timerCallback = jasmine.createSpy("timerCallback");
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
模拟超时(Mocking Timeout)
可以调用jasmine.clock().tick(nTime)
来模拟计时,一旦tick
中设置的时间nTime
,其累计设置的值达到setTimeout
或setInterval
中指定的延时时间,则触发回调函数。
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);
//tick设置的时间,累计到此201ms,因此会触发setInterval中的毁掉函数被调用2次。
jasmine.clock().tick(50);
expect(timerCallback.calls.count()).toEqual(2);
});
异步支持(Asynchronous Support)
调用beforeEach
,it
或者afterEach
时,可以添加一个可选参数(Function
类型,在官方文档的例子中该参数为done
)。当done
函数被调用,表明异步操作的回调函数调用成功;否则如果没有调用done
,表明异步操作的回调函数调用失败,则该Spec不会被调用,且会因为超时退出。
Jasmine等待异步操作完成的默认时间是5s,如果5s内异步操作没有完成,则Spec会因为超时退出。超时时间也可以通过全局的jasmine.DEFAULT_TIMEOUT_INTERVAL
修改[4]。
var value;
// setTimeout代表一个异步操作。
beforeEach(function(done) {
setTimeout(function() {
value = 0;
// 调用done表示回调成功,否则超时。
done();
}, 1);
});
// 如果在beforeEach中的setTimeout的回调中没有调用done,最终导致下面的it因超时而失败。
it("should support async execution of test preparation and expectations", function(done) {
value++;
expect(value).toBeGreaterThan(0);
done();
});
参考资料
[1] Javascript的Unit Test。
[2] 官方文档introduction.js
[3] 官方文档custom_matcher.js
[4] Jasmine——JavaScript 单元测试框架
JavaScript单元测试框架-Jasmine的更多相关文章
- Javascript单元测试框架比较Qunit VS Jasmine
Javascript单元测试框架比较Qunit VS Jasmine 工欲行其事必先利其器,好的单元测试框架是TDD成功的一半.Javascript优秀的测试框架很多, 包括Jasmine,Qunit ...
- javascript单元测试框架mochajs详解
关于单元测试的想法 对于一些比较重要的项目,每次更新代码之后总是要自己测好久,担心一旦上线出了问题影响的服务太多,此时就希望能有一个比较规范的测试流程.在github上看到牛逼的javascript开 ...
- JavaScript单元测试框架JsUnit基本介绍和使用
JavaScript单元测试框架JsUnit基本介绍和使用 XUnit framework XUnit是一套标准化的独立于语言的概念和结构集合,用于编写和运行单元测试(Unit tests). 每一个 ...
- javascript单元测试框架mochajs详解(转载)
章节目录 关于单元测试的想法 mocha单元测试框架简介 安装mocha 一个简单的例子 mocha支持的断言模块 同步代码测试 异步代码测试 promise代码测试 不建议使用箭头函数 钩子函数 钩 ...
- JavaScript单元测试框架:Jasmine
摘抄:http://blog.csdn.net/GuoJiangweigege/article/details/52130589 互联网的快速发展,给web开发人员带来了前所未有的挑战.对于前端开发, ...
- javascript单元测试(转)
1. 什么是单元测试 在计算机编程中,单元测试(又称为模块测试)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作.程序单元是应用的最小可测试部件.在过程化编程中,一个单元就是单 ...
- (译)学习如何构建自动化、跨浏览器的JavaScript单元测试
作者:Philip Walton 译者:Yeaseon 原文链接:点此查看 译文仅供个人学习,不用于任何形式商业目的,转载请注明原作者.文章来源.翻译作者及链接,版权归原文作者所有. ___ 我们都知 ...
- [转]javascript单元测试
1. 什么是单元测试 在计算机编程中,单元测试(又称为模块测试)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作.程序单元是应用的最小可测试部件.在过程化编程中,一个单元就是单 ...
- Java单元测试框架 JUnit
Java单元测试框架 JUnit JUnit是一个Java语言的单元测试框架.它由Kent Beck和Erich Gamma建立,逐渐成为源于KentBeck的sUnit的xUnit家族中为最成功的一 ...
随机推荐
- iOS之04-方法的声明和实现
本次重点学习和理解OC对象方法的声明和定义 代码: /* 计算器类 方法: 1> 返回 π 2> 计算某个整数的平方 3> 计算两个整数的和 */ #import <Found ...
- ACM: Gym 100935B Weird Cryptography - 简单的字符串处理
Weird Cryptography Time Limit:2000MS Memory Limit:65536KB 64bit IO Format:%I64d & %I64u ...
- [Cocos2d-x For WP8]Layer 层
层(CCLayer) 从概念上说,层就是场景里的背景. CCLayer同样是CCNode的子类,通常用addChild方法添加子节点.CCLayer对象定义了可描绘的区域,定义了描绘的规则.C ...
- NOIP欢乐模拟赛 T1 解题报告
小澳的方阵 (matrix.cpp/c/pas) [题目描述] 小澳最近迷上了考古,他发现秦始皇的兵马俑布局十分有特点,热爱钻研的小澳打算在电脑上还原这个伟大的布局. 他努力钻研,发现秦始皇布置兵马俑 ...
- [BZOJ2796][Poi2012]Fibonacci Representation
由于是斐波那契数列,所以$x_i+x_j<=x_k,i<j<k$ 所以猜测可以贪心选择两边近的数处理. #include<cstdio> #include<algo ...
- UVA 11076 - Add Again(组合)
题目链接 脑子抽了,看错题了,神奇的看成没有0了.主要问题把n个数插入m个相同的数,把m个数给分成1-m堆,然后插到n+1空里. #include <cstdio> #include &l ...
- iOS 最全面试题
HTTP/1.0 在HTTP/1.0版本中,并没有官方的标准来规定Keep-Alive如何工作,因此实际上它是被附加到HTTP/1.0协议上,如果客户端浏览器支持Keep-Alive,那么就在HTTP ...
- java-两个大数相加
题目要求:用字符串模拟两个大数相加. 一.使用BigInteger类.BigDecimal类 public static void main(String[] args) { String a=&qu ...
- Odoo 配置快速创建编辑按钮
对于Man2one类型的数据,我们知道,form view中总会显示出一个尾巴似的"create and edit"和一个快速创建的机制,有时候业务人员一不小心就容易创建一个新的行 ...
- 常用JS表单验证方法
/*输入:str返回:如果全是空返回true,否则返回false*/function isNull(str) {if (str == "") return true;var reg ...