AngularJS测试
一 测试工具
1.NodeJS领域:Jasmine做单元测试,Karma自动化完成单元测试,Grunt启动Karma统一项目管理,Yeoman最后封装成一个项目原型模板,npm做nodejs的包依赖管理,bower做javascript的包依赖管理。Java领域:JUnit做单元测试, Maven自动化单元测试,统一项目管理,构建项目原型模板,包依赖管理。
Nodejs让组合变得更丰富,却又在加重我们的学习门槛。唉......
2.Karma
Karma是一个测试工具,它从头开始构建,免去了设置测试方面的负担,这样我们就可以将主要精力放在构建核心应用逻辑上。
Karma产生一个浏览器实例(或者多个不同的浏览器实例),针对不同的浏览器实例运行测试,检测在不同浏览器环境下测试是否通过。Karma与浏览器通过socket.io来联系,这能让Karma保持持续通信。因此Karma提供了关于哪些测试正在运行的实时反馈,提供一份适合人类阅读的输出,告诉我们哪些测试通过、哪些失败或者超时。
Karma产生一个浏览器实例(或者多个不同的浏览器实例),针对不同的浏览器实例运行测试,检测在不同浏览器环境下测试是否通过。Karma与浏览器通过socket.io来联系,这能让Karma保持持续通信。因此Karma提供了关于哪些测试正在运行的实时反馈,提供一份适合人类阅读的输出,告诉我们哪些测试通过、哪些失败或者超时。
Karma测试运行器同时支持单元测试和端到端测试。
3.Karma安装
如果你已经安装了NodeJS和npm,就可以通过npm命令来安装karma
安装命令:npm install -g karma
测试是否安装成功:karma start
如果成功安装karma,则可以通过浏览器看到karma界面
4.karma+jasmine配置
初始化karma配置文件karma.conf.js:karma init
安装集成包karma-jasmine:npm install karma-jasmine
karma安装好后,后面就是具体的jasmine测试了。
二 jasmine测试
1.尽管Karma支持多种测试框架,但默认的选项是Jasmine。Jasmine是一个用于测试JavaScript代码的行为驱动开发框架。
下载发布的安装包:jasmine-standalone-2.2.0.zip
下载后的目录结构:
(1)lib:存放了运行测试案例所必须的文件,其内包含jasmine-2.2.0文件夹。可以将不同版本的Jasmine放在lib下,以便使用时切换。
jasmine.js:整个框架的核心代码。
jasmine-html.js:用来展示测试结果的js文件。
boot.js:jasmine框架的的启动脚本。需要注意的是,这个脚本应该放在jasmine.js之后,自己的js测试代码之前加载。
jasmine.css:用来美化测试结果。
boot.js:jasmine框架的的启动脚本。需要注意的是,这个脚本应该放在jasmine.js之后,自己的js测试代码之前加载。
jasmine.css:用来美化测试结果。
(2)spec:存放测试脚本。
PlayerSpec.js:就是针对src文件夹下的Player.js所写的测试用例。
SpecHelper.js:用来添加自定义的检验规则,如果框架本身提供的规则(诸如toBe,toNotBe等)不适用,就可以额外添加自己的规则(在本文件中添加了自定义的规则toBePlaying)。
SpecHelper.js:用来添加自定义的检验规则,如果框架本身提供的规则(诸如toBe,toNotBe等)不适用,就可以额外添加自己的规则(在本文件中添加了自定义的规则toBePlaying)。
(3)src:存放需要测试的js文件。Jasmine提供了一个Example(Player.js,Song.js)。
(4)pecRunner.html:运行测试用例的环境。它将上面3个文件夹中一些必要的文件都包含了进来。如果你想将自己的测试添加进来的话,那么就修改相应的路径。
2.核心概念
(1)细则套件Suites
Suite表示一个测试集,以函数describe(string, function)封装。describe函数是Jasmine套件定义的一个全局函数,所以可以在测试中直接调用。
describe()函数带有两个参数,一个字符串,一个函数。字符串是待建立的细则(spec)套件名称或者描述,函数封装了测试套件。
describe()函数带有两个参数,一个字符串,一个函数。字符串是待建立的细则(spec)套件名称或者描述,函数封装了测试套件。
可以嵌套这些describe()函数,这样我们可以创建一个测试树来执行那些在测试中设置的不同条件。如:
describe('Unit test: MainController', function() {
describe('index method', function() {
// 细则放这里
});
});
describe('Unit test: MainController', function() {
describe('index method', function() {
// 细则放这里
});
});
一个Suite(describe)包含多个Specs(it),一个Specs(it)包含多个断言(expect)。
使用describe()函数把相关的细则分组是个不错的主意。在每个describe()块运行时,这些字符串会沿着细则的名称链接起来。因此,上面这个例子的标题就会变成“Unit test:MainController index method.”然后,这些describe()块的标题就会被追加到细则的标题上。设计这个步骤的目的是让我们以完整句子来阅读细则的,所以把测试命名成可读的英文就很重要了。
(2)定义一个细则
Spec表示测试用例,以it(string, function)函数封装。我们通过调用it()函数来定义一个细则。这个函数也是在Jasmine测试套件中定义的全局函数,所以可以从测试中直接调用。
it()函数带有两个参数:一个字符串,是细则的标题或者描述;一个函数,包含了一个或多个用于测试代码功能的预期。
这些预期都是函数,执行时评估为true或false。一个所有预期都为true的测试就算是一条通过的细则,一条细则有一个或者多个预期为false的话,就是个失败的测试。
一个简单的测试可能像这样:
describe('A spec suite', function() {
it('contains a passing spec', function() {
expect(true).toBe(true);
});
});
这个细则的标题,追加到describe()标题之后,就成为了“一个细则套件包含一条已通过的细则”。
(3)预期
describe('A spec suite', function() {
it('contains a passing spec', function() {
expect(true).toBe(true);
});
});
这个细则的标题,追加到describe()标题之后,就成为了“一个细则套件包含一条已通过的细则”。
(3)预期
测试应用时,我们会想要断言条件在应用的不同阶段是符合我们期望的。我们要写的这个测试读起来就像这样:“如果我们点击这个按钮,就期望有这个结果。”例如,“如果我们导航到首页,我们期望欢迎信息会被渲染出来。”
使用expect()函数来建立预期。expect()函数带有一个单值参数。这个参数被称为真实值。
要建立一个预期,我们给它串联一个带单值参数的匹配器函数,这个参数就是期望值。
这些匹配器函数实现了一个在真实值和期望值之间的布尔比较。可以通过在调用匹配器之前调一个not来创建测试的否定式。
这些匹配器函数实现了一个在真实值和期望值之间的布尔比较。可以通过在调用匹配器之前调一个not来创建测试的否定式。
(4)内置匹配器matchers
常用的Matchers有:
toBe():相当于===比较。
toNotBe()
toBeDefined():检查变量或属性是否已声明且赋值。
toBeUndefined()
toBeNull():是否是null。
toBeTruthy():如果转换为布尔值,是否为true。
toBeFalsy()
toBeLessThan():数值比较,小于。
toBeGreaterThan():数值比较,大于。
toBe():相当于===比较。
toNotBe()
toBeDefined():检查变量或属性是否已声明且赋值。
toBeUndefined()
toBeNull():是否是null。
toBeTruthy():如果转换为布尔值,是否为true。
toBeFalsy()
toBeLessThan():数值比较,小于。
toBeGreaterThan():数值比较,大于。
toEqual():相当于==,注意与toBe()的区别。
toNotEqual()
toContain():数组中是否包含元素(值)。只能用于数组,不能用于对象。
toBeCloseTo():数值比较时定义精度,先四舍五入后再比较。
toContain():数组中是否包含元素(值)。只能用于数组,不能用于对象。
toBeCloseTo():数值比较时定义精度,先四舍五入后再比较。
toHaveBeenCalled()
toHaveBeenCalledWith()
toMatch():按正则表达式匹配。
toNotMatch()
toHaveBeenCalledWith()
toMatch():按正则表达式匹配。
toNotMatch()
toThrow():检验一个函数是否会抛出一个错误
所有的matchers匹配器支持添加 .not反转结果: expect(x).not.toEqual(y);
举例:
describe('A spec suite', function() {
it('contains a passing spec', function() {
var value = "<h2>Header element: welcome</h2>";
expect(value).toMatch(/welcome/);
expect(value).toMatch('welcome');
expect(value).not.toMatch('goodbye');
});
});
it('contains a passing spec', function() {
var value = "<h2>Header element: welcome</h2>";
expect(value).toMatch(/welcome/);
expect(value).toMatch('welcome');
expect(value).not.toMatch('goodbye');
});
});
describe('A spec suite', function() {
it('contains a passing spec', function() {
var arr = [1,2,3,4];
expect(arr).toContain(4);
expect(arr).not.toContain(12);
});
});
it('contains a passing spec', function() {
var arr = [1,2,3,4];
expect(arr).toContain(4);
expect(arr).not.toContain(12);
});
});
describe('A spec suite', function() {
it('contains a passing spec', function() {
expect(function() {
return a + 10;
}).toThrow();
expect(function() {
return 2 + 10;
}).not.toThrow();
});
});
it('contains a passing spec', function() {
expect(function() {
return a + 10;
}).toThrow();
expect(function() {
return 2 + 10;
}).not.toThrow();
});
});
(5)自定义matcher匹配器
自定义Matcher(被称为Matcher Factories)实质上是一个函数(该函数的参数可以为空),该函数返回一个闭包,该闭包的本质是一个compare函数,compare函数接受2个参数:actual value 和 expected value。
compare函数必须返回一个带pass属性的结果Object,pass属性是一个Boolean值,表示该Matcher的结果(为true表示该Matcher实际值与预期值匹配,为false表示不匹配),也就是说,实际值与预期值具体的比较操作的结果,存放于pass属性中。
(6)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。
(7)this关键字
除了在describe函数开始定义变量,用于各it函数共享数据外,还可以通过this关键字来共享数据。
在每一个Spec的生命周期(beforeEach->it->afterEach)的开始,都将有一个空的this对象(在开始下一个Spec周期时,this会被重置为空对象)。
3.高级特性
1.Spy
Spy能监测任何function的调用和方法参数的调用痕迹。需使用2个特殊的Matcher:
toHaveBeenCalled:可以检查function是否被调用过,
toHaveBeenCalledWith: 可以检查传入参数是否被作为参数调用过。
2.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();
});
});
3.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);
});
});
4.全局匹配谓词
(1)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));
});
});
});
(2)
jasmine.anything
jasmine.anything
用于检测实际值是否为null
或undefined
,如果不为null
或undefined
,则返回true
。it("matches anything", function() {
expect(1).toEqual(jasmine.anything());});
(3)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
}));
});
5.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();
});
6.模拟超时(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);
});
三 模拟
1.什么是mock
在开始写测试之前,我们需要理解测试的一个核心特性:模拟。模拟允许我们在受控环境下定义模拟对象来模仿真实对象的行为。AngularJS提供了自己的模拟库:angular-mocks,位于angular-mock.js文件中,因此如果要在单元测试中建立模拟对象,就必须确保在Karma配置中,即test/karma.conf.js文件的file数组中包含了angular-mock.js。
2.ng-mock中的一些常用的方法
(1)angular.mock.module
此方法非常方便调用,因为angular.mock.module函数被发布在全局作用域的window接口上了。
module是用来配置inject方法注入的模块信息,参数可以是字符串,函数,对象,它一般用在beforeEach方法里,因为这个可以确保在执行测试任务的时候,inject方法可以获取到模块配置。
describe('myApp',function(){
//模拟'myApp'angular模块
beforeEach(angular.mock.module('myApp'));
it('....')
});
建立了模拟的angular模块之后,可以把连接到这个模块上的任意服务注入到测试代码中。在我们的测试代码中,注入依赖关系很重要,因为我们隔离了想要测试的功能。
(2)angular.mock.inject
inject函数也是在window对象上的,为的是全局访问,因此可以直接调用inject。
inject是用来注入上面配置好的ng模块,方便在it的测试函数里调用。
describe('myApp',function(){
var scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.inject(function($rootscope){
scope=$rootscope.$new();
});
it('...');
});
通常我们会用将引入进测试时使用的名字来保存它。比如说,如果我们在测试一个服务,可以注入这个服务,然后把它的引用用一种稍微不同的命名方案存储起来。在注入的服务名称两端使用下划线,当它被注入时,注入器会忽略它的名称。
describe('myApp',function(){
var scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.inject(function(_myService_){
myService=_myService_;
});
it('...');
});
3.模拟$httpBackend
angualr内置了$httpBackend模拟库,这样我们可以在应用中模拟任何外部的XHR请求,避免在测试中创建昂贵的$http请求。
如:
var app = angular.module('Application', []);
app.controller('MainCtrl', function($scope, $http) {
$http.get('Users/users.json').success(function(data){
$scope.users = data;
});
$scope.text = 'Hello World!';
});
测试:
describe('MainCtrl', function() {
//我们会在测试中使用这个scope
var scope, $httpBackend;
//模拟我们的Application模块并注入我们自己的依赖
beforeEach(angular.mock.module('Application'));
//模拟Controller,并且包含 $rootScope 和 $controller
beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_) {
//设置$httpBackend冲刷$http请求
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', 'Users/users.json').respond([{
id: 1,
name: 'Bob'
}, {
id: 2,
name: 'Jane'
}]);
//创建一个空的 scope
scope = $rootScope.$new();
//声明 Controller并且注入已创建的空的 scope
$controller('MainCtrl', {
$scope: scope
});
}));
// 测试从这里开始
it('should have variable text = "Hello World!"', function() {
expect(scope.text).toBe('Hello World!');
});
it('should fetch list of users', function() {
$httpBackend.flush();
expect(scope.users.length).toBe(2);
expect(scope.users[0].name).toBe('Bob');
//输出结果以方便查看
for(var i=0;i<scope.users.length;i++){
console.log(scope.users[i].name);
}
});
});
可以使用$httpBackend.when和$httpBackend.expect提前设置请求的伪数据,最后在请求后执行$httpBackend.flush就会立即执行完成http请求。
4.$httpBackend常用方法
(1)when :新建一个后端定义(backend definition)。
when(method, url, [data], [headers]);
(2)expect :新建一个请求期望(request expectation)。
expect(method, url, [data], [headers]);
method表示http方法注意都需要是大写(GET, PUT…);
url请求的url可以为正则或者字符串;
data请求时带的参数,
headers请求时设置的header。
如果这些参数都提供了,那只有当这些参数都匹配的时候才会正确的匹配请求。when和expect都会返回一个带respond方法的对象。respond方法有3个参数status,data,headers通过设置这3个参数就可以伪造返回的响应数据了。
$httpBackend.when与$httpBackend.expect的区别在于:$httpBackend.expect的伪后台只能被调用一次(调用一次后会被清除),第二次调用就会报错,而且$httpBackend.resetExpectations可以移除所有的expect而对when没有影响。
快捷方法:when和expect都有对应的快捷方法whenGET, whenPOST,whenHEAD, whenJSONP, whenDELETE, whenPUT; expect也一样。
使用快捷方法进行测试:
$httpBackend.whenGET('/someUrl').respond({name:'wolf'},{'X-Record-Count':100}); //声明Mock服务,模拟后端服务器行为
//调用网络接口
$http.get('/someUrl').success(function(data){
expect(data.name).toBe('wolf');
});
//刷新一次,模拟后端返回请求,在调用这个命令之前,success中的回调函数不会被执行
$httpBackend.flush();