AngularJS中的一个典型的Controller
在AngularJS中,Controller主要用于hold一些跟view的有关的状态,以及数据模型,比如界面上某些元素是否展示,以及展示那些内容等。通常来说,Controller会依赖与一个Service来提供数据:
1
2
3
4
5
6
app . controller ( 'EventController' , [ '$scope' , 'EventService' ,
function ( $scope , EventService ) {
EventService . getEvents (). then ( function ( events ) {
$scope . events = events ;
});
}]);
而service本身则需要通过向后台服务发送请求来获取数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app . factory ( 'EventService' , [ '$http' , '$q' ,
function ( $http , $q ) {
return {
getEvents : function () {
var deferred = $q . defer ();
$http . get ( '/events.json' ). success ( function ( result ) {
deferred . resolve ( result );
}). error ( function ( result ) {
deferred . reject ( result );
});
return deferred . promise ;
}
};
}]);
通常的做法是返回一个promise 对象,然后当数据准备完整之后,controller的then会被执行。
那么对于这种情况(在AngularJS中,算是一个非常典型的场景),我们如何进行单元测试呢?
测试依赖与Service的Controller
通常来讲,在单元级别的测试中,我们肯定不希望Service真正的发送请求,这样就变成了集成测试,而且前端的开发完全依赖与后台的开发进度/稳定程度等。
所以我们需要做一个假的Service,这个假的Service仅仅在测试中存在:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var app = angular . module ( 'MyApp' );
describe ( "EventController" , function () {
var scope , q ;
var controllerFactory ;
var mockSerivce = {};
var events = [ "Event1" , "Event2" , "Event3" ];
beforeEach ( function () {
module ( "MyApp" );
inject ( function ( $rootScope , $controller , $q ) {
controllerFactory = $controller ;
scope = $rootScope . $new ();
q = $q ;
});
});
beforeEach ( function () {
var deferred = q . defer ();
deferred . resolve ( events );
mockSerivce . getEvents = jasmine . createSpy ( 'getEvents' );
mockSerivce . getEvents . andReturn ( deferred . promise );
});
function initController () {
return controllerFactory ( 'EventController' , {
$scope : scope ,
EventService : mockSerivce
});
}
it ( "should have a events list" , function () {
initController ();
scope . $digest ();
expect ( scope . events . length ). toEqual ( 3 );
expect ( scope . events ). toEqual ( events );
});
});
此处有很多值得注意的事情:
在何处实例化Controller
不要在注入beforeEach
中初始化Controller,很多示例中都会在注入了$controller
之后紧接着实例化Controller,如果Controller有多个外部的依赖的话,那么在beforeEach
中的代码将越来越多,而且读每一个测试用例时会有一些疑惑。
一个好的做法是将依赖注入到describe
中的临时变量中,然后将初始化的动作延后到一个函数中:
1
2
3
4
5
6
function initController () {
return controllerFactory ( 'EventController' , {
$scope : scope ,
EventService : mockSerivce
});
}
如何mock一个service
由于在AngularJS中,Service一般会返回一个promise 对象。因此在测试时需要有一些技巧来绕过:
1
2
3
4
5
6
7
8
var events = [ "Event1" , "Event2" , "Event3" ];
beforeEach ( function () {
var deferred = q . defer ();
deferred . resolve ( events );
mockSerivce . getEvents = jasmine . createSpy ( 'getEvents' );
mockSerivce . getEvents . andReturn ( deferred . promise );
});
这样,当使用注入EventService.getEvents().then(callback)
的地方就可以访问到此处的promise对象了。
如果添加了新的用例,
1
2
3
4
5
6
7
app . controller ( 'EventController' , [ '$scope' , 'EventService' ,
function ( $scope , EventService ) {
EventService . getEvents (). then ( function ( events ) {
$scope . events = events ;
$scope . recentEvent = $scope . events [ 0 ];
});
}]);
则在用例开始完成创建Controller的动作即可:
1
2
3
4
5
it ( "should have a recent event" , function () {
initController ();
scope . $digest ();
expect ( scope . recentEvent ). toEqual ( "Event1" );
});
完整的代码请看此处