测试应用
1.测试路由
我们需要检测路由是否在运作,是否找到了,或者是404了。我们要确认路由事件触发了,预期的模板是否真的加载了。既然路由会改变页面的地址(URL)和页面内容,我们需要检测路由是否被加载了,页面是否找到了,在这中间发生了什么。
一段简单的路由代码:
angular.module(‘myApp‘, [‘ngRoute‘])
.config(function($routeProvider) {
$routeProvider
.when(‘/‘, {
templateUrl: ‘views/main.html‘,
controller: ‘HomeController‘})
.when(‘/login‘, {
templateUrl: ‘views/login.html‘,
controller: ‘LoginController‘})
.otherwise({redirectTo ‘/‘});
})
测试路由我们需要做以下几件事:
(1)注入$route、$location和$rootScope服务。
(2)建立一个模拟的后端来处理XHR,获取模板代码。
(3)设置一个地址,运行一个$digest生命周期
Step1:在测试中引用服务,并设置一个模拟后端,从templateUrl获取模板。可以用$httpBackend来创建断言来判断指定模板被加载了.
describe(‘Routes test‘, function() {
// 在测试中模拟模块
beforeEach(module(‘myApp‘));
var location, route, rootScope;
beforeEach(inject(
function(_$location_, _$route_, _$rootScope_) {
location = _$location_;
route = _$route_;
rootScope = _$rootScope_;
}));
describe(‘index route‘, function() {
beforeEach(inject(
function($httpBackend) {
$httpBackend.expectGET(‘views/home.html‘)
.respond(200, ‘main HTML‘);
}));
// 我们的测试代码放在这里
});
});
Step2:
为了用单元测试来测试路由,我们需要模拟路由在生产中的运作。路由借助于digest生命周期来运行,当location被设置以后,它使用一个
digest循环周期来处理路由,改变页面内容,然后结束本次路由。了解了这些之后,我们就需要解释测试中路径的变更。
在测试中,我们打算在应用的index路由上测试两个状态:当用户导航到首页时,指定的控制器会给他们显示首页;当用户导航到一个未知路由时,他们会按照在otherwise函数中定义的那样被带到首页。
我们可以通过建立$location服务来传递路径的方式测试这些条件。为了触发location请求,我们要运行一个digest周期(在$rootScope上),然后检测控制器是符合预期的(在本例中,是“HomeController”)。
it(‘should load the index page on successful load of /‘,
function() {
location.path(‘/‘);
rootScope.$digest(); // 调用digest循环
expect(route.current.controller)
.toBe(‘HomeController‘)
});
it(‘should redirect to the index path on non-existent
route‘, function() {
location.path(‘/definitely/not/a/_route‘);
rootScope.$digest();
expect(route.current.controller)
.toBe(‘HomeController‘)
});
2.测试控制器
在建立单元测试的过程中,需要确保:
建立了测试来模拟模块;
用一个已知的作用域实例来存储控制器的一个实例;
基于作用域来测试我们的预期。
要初始化一个控制器实例,需要使用$new()方法从$rootScope创建某作用域的一个新实例。这个新实例会建立Angular在运行时使用的作用域继承。有了这个作用域,就可以初始化一个新的控制器,把这个作用域作为控制器的$scope传递过去。
describe(‘Unit controllers: ‘, function(){
// 模拟myApp模块
beforeEach(module(‘myApp‘));
describe(‘FrameController‘, function() {
// 局部变量
var FrameController, scope;
beforeEach(inject(
function($controller, $rootScope) {
// 创建子作用域
scope = $rootScope.$new();
// 创建FrameController的新实例
FrameController = $controller(‘FrameController‘,
{ $scope: scope });
}));
// 我们的测试代码放在这里
});
});
在FrameController里,我们有一个时钟在应用顶部显示当前时间。我们也可以访问一个用户和他的时区。具体的控制器的代码如下
angular.module(‘myApp.controllers‘, [])
.controller(‘FrameController‘,
function($scope, $timeout) {
$scope.time = {
today: new Date()
};
$scope.user = {
timezone: ‘US/Pacific‘
}
var updateClock = function() {
$scope.time.today = new Date();
};
var tick = function() {
$timeout(function() {
$scope.$apply(updateClock);
tick();
}, 1000);
}
tick();
});
我们要测试控制器的两个功能:时间已被定义;用户已被定义,并且有时区。
it(‘should have today set‘, function() {
expect(scope.time.today).toBeDefined();
});
it(‘should have a user set‘, function() {
expect(scope.user).toBeDefined();
});
再举个简单易懂的例子:
//创建一个scope对象,true表示它不会继承父级的变量,以免带来困扰
var scope=$rootScope.$new(true);
//找到SomeController,并对scope进行初始化
$controller(‘$SomeController‘,{$scope:scope}); //在这里也可以注入其他被mock的service或resolve的对象
//期待scope上应该出现一个name属性,其值为some one
$expect(scope.name).toEqual(‘some one‘);
//期待scope上应该出现一个greeting函数,调用它后的值为hello,some one
$expect(scope.greeting()).toEqual(‘hello,some one‘);
3.测试工厂和服务
Service可以是函数,可以是类,也可以是变量,所以测试起来没什么固定的形式,但是步骤和原理是相似的。
注入这个Service,把它保存为一个变量;
如果是函数,就调用它;如果是类,就new它,然后检查成员;如果是变量,就对比它的值。
var myClass;
//注入
beforeEach(inject(function(_MyClass_){
MyClass=_MyClass_; //赋给外部变量,以便后面的测试中使用
}));
it(‘someMethod should be 2‘,function(){
var obj=new MyClass();
expect(obj.someMethod()).toEqual(2);
});
4.测试过滤器
首先,要访问过滤器,只需简单地把$filter服务注入到我们的测试中。这样我们就得到了一个在此过程中查找过滤器的途径:
describe(‘Unit: Filter tests‘, function() {
var filter;
// 在测试中模拟我们的引用
beforeEach(module(‘myApp‘));
beforeEach(inject(function($filter) {
filter = $filter;
}));
});
有了对控制器的访问,在过滤器的输出上设置预期就是很容易的事了。
it(‘should give us two decimal points‘,
function() {
expect(filter(‘number‘)(123, 2)).toEqual(‘123.00‘);
});
5.测试模板
测试模板时,我们着重于确保:特定的内容模板被加载了,模板中特定的数据也在视图中显示了。
可以建立一个断言:模板正常加载了。要做到这个,需要建立测试来期望对主页模板的一个请求,并且执行一个视图的变更来验证它是不是真的加载了。
describe(‘Unit: Templates‘, function() {
var $httpBackend,
location,
route,
rootScope;
beforeEach(module(‘myApp‘));
beforeEach(inject(
function(_$rootScope_, _$route_, _$httpBackend_, _$location_){
location = _$location_;
rootScope = _$rootScope_;
route = _$route_;
$httpBackend = _$httpBackend_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
// 我们的测试代码放在这里
});
现在,可以建立测试来反映导航到应用不同部分时的预期。
it(‘loads the home template at /‘, function() {
$httpBackend.expectGET(‘templates/home.html‘)
.respond(200);
location.path(‘/‘);
rootScope.$digest(); // 调用digest循环
$httpBackend.flush();
});
it(‘loads the dashboard template at /dashboard‘, function() {
$httpBackend.expectGET(‘templates/dashboard.html‘)
.respond(200);
location.path(‘/dashboard‘);
rootScope.$digest(); // 调用digest循环
$httpBackend.flush();
});
注意,我们并未在测试中返回一个模板(是.respond(200),而不是.respond(200, "\<div\>\</div\>"))。鉴于我们只是在验证模板是否在请求中加载了,没有必要担心到底显示了什么。
6.测试事件
当对事件的触发进行单元测试时,我们感兴趣的是它们究竟调用了什么,还有是否真的调用了正确的事件。其次,我们主要关心处理程序有它们需要的数据。
使用Jasmine的辅助方法spnOn()可以非常容易地建立事件测试。设想我们在测试一个控制器,它触发了一个$emit函数。基于这个函数,我们可以建立一个预期:事件被触发了,并且用我们所感兴趣的任意参数调用了。
describe(‘myApp‘, function() {
var scope;
beforeEach(angular.mock.module(‘myApp‘));
beforeEach(angular.mock.inject(function($rootScope) {
scope = $rootScope.$new();
});
});
测试建好之后,就可以简单地在作用域上为$emit或者$broadcase事件设置一个spyOn()调用了。
// ...
});
it(‘should have emit called‘, function() {
spyOn(scope, "$emit");
scope.closePanel(); // 示例
// 或者任意可能导致emit被调用的事件
expect(scope.$emit)
.toHaveBeenCalledWith("panel:closed",
panel.id);
});
我们也可以测试事件:设置一个事件触发后调用的$on()监听器。要执行$broadcast方法,
可以简单地在作用域上调用$broadcast,并且对事件将会导致的作用域变化建立一个预期。
// ...
it(‘should set the panel to closed state‘,
function() {
scope.$broadcast("panel:closed", 1);
expect(scope.panel.state).toEqual("closed");
});