AngularJS单元测试系列-指令的单元测试

指令的单元测试有几个关键的步骤,可以设置一个检查列表,然后看看每一条是否都执行了即可:
(1)在单元测试中注入$compile服务
(2)创建待测试的指令的作用域
(3)单元测试中没有服务器,所以如果待测试的指令中用到templateUrl,需要在$httpBackend中加入期待返回的内容,用于模拟测试中的模板。
(4)创建指令实例 —— HTML元素来触发待测试的指令(通过$compile服务和创建的作用域来编译HTML元素)
(5)更新HTML中的数据绑定,加载并渲染$httpBackend中定义的HTML模板,以便下一步执行单元测试

前5项适用于任何指令的单元测试,第6项涉及页面渲染和业务逻辑,不同的指令中各有不同:
(6)为指令的渲染方式以及link函数中定义的函数设置预期值(expect)

注释:单元测试中不会真的加载HTML,因为服务器不存在。

以stockWidge指令为例:
angular.module('stockMarketApp', [])
.directive('stockWidge', [function() {
return {
templateUrl: 'stock.html', //渲染逻辑定义在stock.html中,通过templateUrl加载
restrict: 'A', //指令只能通过属性的形式调用
scope: { //有一个隔离作用域
stockData: '=',
stockTitle: '@',
whenSelect: '&'
},
link: function(scope, elem, attrs) { //link函数中定义了两个函数
scope.getChange = function(stock) {
return Math.ceil( ((stock.price - stock.previous) / stock.previous) * 100);
};

scope.onSelect = function() {
scope.whenSelect({
stockName: scope.stockData.name,
stockPrice: scope.stockData.price,
stockPrevious: scope.stockData.previous
});
};
}
};
}]);

下面编写单元测试用例:

<1>测试指令的渲染过程(重点是指令本身的作用域isolateScope/指令所对应的元素的父级作用域scope)
describle('Stock Widget Directive Rendering', function() {
beforeEach(module('stockMarketApp'));
var compile, mockBackend, rootScope;

//第一步:注入测试必要的用于创建和测试指令的服务
beforeEach(inject(function($compile, $httpBackend, $rootScope) {
compile: $compile, //用于创建指令的实例
mockBackend: $httpBackend, //用于模拟和处理服务器请求以加载模板
rootScope: $rootScope //用于创建指令的作用域
}));

it('should render HTML based on scope correctly', function() {
//第二步:创建指令实例对应的作用域(element元素的父级作用域scope)并设置必要的变量
var scope = rootScope.new();
scope.myStock = {
name: 'Best Stock',
price: 100,
previous: 200
};
scope.title = 'the best';

//第三步:在$httpBackend中指定加载模板以及其内容的预期值(提供HTML以测试元素是否被正确渲染,数据是否准确)
mockBackend.expectGET('stock.html').respond(
'<div ng-bind="stockTitle"></div>' +
'<div ng-bind="stockData.price"></div>'
);

//第四步:创建一个指令实例,编译触发指令的HTML,这会返回一个经过编译的函数,我们可以通过作用域来调用这个函数
var element = compile(
'<div stock-widget stock-data="myStock" stock-title="This is {{title}}"></div>'
)(scope);

//第五步:AngularJS更新HTML中的数据绑定,$httpBackend中定义的HTML会被加载并渲染出来,才能进一步做单元测试
scope.digest(); //我们手动创建并编译了HTML,需要手动触发并通知AngularJS来用最新数据更新模板
mockBackend.flush();

//第六步:设置预期值并测试渲染结果
expect(element.html).toEqual(
'<div ng-bind="stockTitle" class="ng-binding">This is the best</div>' +
'<div ng-bind="stockData.price" class="ng-binding">100</div>'
);
});
});

<2>测试指令的业务逻辑和行为
describle('Stock Widget Directive Behavior', function(){
beforeEach(module('stockMarketApp'));
var compile, mockBackend, rootScope;

//第一步:注入必要的服务
beforeEach(inject(function($compile, $httpBackend, $rootScope){
compile = $compile;
mockBackend = $httpBackend;
rootScope = $rootScope;
}));

it('should have functions and data on scope correctly', function(){
//第二步:创建指令实例对应的作用域并设置必要的变量
var scope = rootScope.$new();
var scopeClickCalled = '';
scope.myStock = {
name: 'Best Stock',
price: 100,
previous: 200
};
scope.title = 'the best';
scope.userClick = function(stockPrice, stockPrevious, stockName){
scopeClickCalled = stockPrice + ';' + stockPrevious + ';' + stockName;
};

//第三步:模拟加载模板
mockBackend.expectGET('stock.html').respond(
'<div ng-bind="stockTitile"></div>' +
'<div ng-bind="stockData.price"></div>'
);

//第四步:创建一个指令实例,编译触发指令的HTML
var element = compile(
'<div stock-widget stock-data="myStock" stock-title="This is {{title}}" whenSelect="userClick(stockPrice, stockPrevious, stockName)"></div>'
)(scope);

//第五步:更新数据绑定
scope.$digest();
mockBackend.flush();

//第六步:设置预期值并测试指令中的函数
//请求元素的隔离作用域,不同于element.scope —— 指令所对应元素的父作用域。而isolateScope是指令本身的作用域。
var compiledElementScope = element.isolateScope();
expect(compiledElementScope.stockData).toEqual(scope.stockData);
expect(compiledElementScope.getChange(compiledElementScope.stockData)).toEqual(-50);

expect(scopeClickCalled).toEqual('');
compiledElementScope.onSelect();
expect(scopeClickCalled).toEqual('100;200;Best Stock');
});
});

其他
(1)如何测试真正的HTML渲染?
1、使用templateUrl时,不会真的去加载模板,我们可以使用测试模板来测试指令,如上面的例子中所示。
2、使用template代替templateUrl,这样会将模板作为指令的一部分加载进来,但模板较大时会比较混乱。
3、使用类似Grunt-HTML2JS工具,将接收的HTML模板转换成AngularJS的服务和工厂类,这样就可以将模板加载为服务并让它们能在单元测试中使用。
(2)如何处理指令中的依赖?
AngularJS会自动找出依赖并将它们自动注入。也可以将服务注入到单元测试中。

posted @ 2017-01-03 16:16  shiddong  阅读(658)  评论(0编辑  收藏  举报