
AngularJS设计时考虑了可测试性。 依赖注入是该框架的显着特征之一,它使单元测试更加容易。 AngularJS定义了一种将应用程序整洁地模块化并将其划分为不同组件(如控制器,指令,过滤器或动画)的方法。 这种开发模型意味着各个部分可以独立工作,并且该应用程序可以在很长一段时间内轻松扩展。 由于可扩展性和可测试性齐头并进,因此测试AngularJS代码很容易。
根据单元测试的定义, 被测系统应单独进行测试。 因此,系统所需的任何外部对象都必须替换为模拟对象。 顾名思义,模拟对象并不执行实际的任务; 而是用来满足被测系统的期望。 如果您需要复习模拟知识,请参阅我以前的文章之一: AngularJS Tests中的模拟依赖项 。
在本文中,我将分享有关AngularJS中的测试服务,控制器和提供程序的提示。 这些代码片段是使用Jasmine编写的,可以与Karma测试运行程序一起运行。 您可以从我们的GitHub repo下载本文中使用的代码,您还将在其中找到有关运行测试的说明。
测试服务
服务是AngularJS应用程序中最常见的组件之一。 它们提供了一种在中央位置定义可重复使用的逻辑的方法,从而无需一遍又一遍地重复相同的逻辑。 服务的单例性质使跨多个控制器,指令甚至其他服务共享同一数据成为可能。
服务可以依赖于一组其他服务来执行其任务。 假设一个名为A的服务依赖于服务B,C和D来执行其任务。 在测试服务A时,必须将B,C和D依赖项替换为模拟。
我们通常模拟所有依赖项,除了某些实用程序服务,例如$rootScope和$parse 。 我们使用jasmine.createSpy()在测试中必须检查的方法上创建间谍(在Jasmine中, jasmine.createSpy()称为间谍jasmine.createSpy() ,这将返回一个全新的函数。
让我们考虑以下服务:
angular.module('services', [])
.service('sampleSvc', ['$window', 'modalSvc', function($window, modalSvc){
this.showDialog = function(message, title){
if(title){
modalSvc.showModalDialog({
title: title,
message: message
});
} else {
$window.alert(message);
}
};
}]);
该服务只有一种方法( showDialog )。 根据此方法接收的输入值,它调用作为依赖项( $window或modalSvc )注入到其中的两个服务之一。
要测试sampleSvc我们需要模拟两个相关服务,加载包含我们服务的angular模块并获取对所有对象的引用:
var mockWindow, mockModalSvc, sampleSvcObj;
beforeEach(function(){
module(function($provide){
$provide.service('$window', function(){
this.alert= jasmine.createSpy('alert');
});
$provide.service('modalSvc', function(){
this.showModalDialog = jasmine.createSpy('showModalDialog');
});
});
module('services');
});
beforeEach(inject(function($window, modalSvc, sampleSvc){
mockWindow=$window;
mockModalSvc=modalSvc;
sampleSvcObj=sampleSvc;
}));
现在我们可以测试showDialog方法的行为。 我们可以为该方法编写的两个测试用例如下:
- 如果没有传递
title参数,它将调用alert - 如果同时存在
title和message参数,它将调用showModalDialog
以下代码段显示了这些测试:
it('should show alert when title is not passed into showDialog', function(){
var message="Some message";
sampleSvcObj.showDialog(message);
expect(mockWindow.alert).toHaveBeenCalledWith(message);
expect(mockModalSvc.showModalDialog).not.toHaveBeenCalled();
});
it('should show modal when title is passed into showDialog', function(){
var message="Some message";
var title="Some title";
sampleSvcObj.showDialog(message, title);
expect(mockModalSvc.showModalDialog).toHaveBeenCalledWith({
message: message,
title: title
});
expect(mockWindow.alert).not.toHaveBeenCalled();
});
这种方法没有太多的逻辑可以测试,而典型的Web应用程序中的服务通常会包含很多功能。 您可以使用本技巧中演示的技术来模拟和获取对服务的引用。 服务测试应涵盖编写服务时假定的所有可能情况。
工厂和价值也可以使用相同的技术进行测试。
测试控制器
测试控制器的设置过程与服务的设置过程完全不同。 这是因为控制器不是可注入的,而是在路由加载或编译ng-controller指令时自动实例化的。 由于我们没有在测试中加载视图,因此我们需要手动实例化正在测试的控制器。
由于控制器通常与视图相关联,因此控制器中方法的行为取决于视图。 此外,在编译视图后,可能还会将一些其他对象添加到范围中。 最常见的示例之一是表单对象。 为了使测试按预期工作,必须手动创建这些对象并将其添加到控制器中。
控制器可以是以下类型之一:
- 与
$scope使用的控制器 - 控制器与
Controller as语法
如果您不确定两者之间的区别,可以在此处阅读有关它的更多信息 。 无论哪种方式,我们都将讨论这两种情况。
用$ scope测试控制器
考虑以下控制器:
angular.module('controllers',[])
.controller('FirstController', ['$scope','dataSvc', function($scope, dataSvc) {
$scope.saveData = function () {
dataSvc.save($scope.bookDetails).then(function (result) {
$scope.bookDetails = {};
$scope.bookForm.$setPristine();
});
};
$scope.numberPattern = /^\d*$/;
}]);
要测试此控制器,我们需要通过传入$scope对象和服务的dataSvc对象( dataSvc )创建控制器的实例。 由于服务包含异步方法,因此我们需要使用我在上一篇文章中概述的模拟承诺技术来模拟该方法。
以下代码段dataSvc了dataSvc服务:
module(function($provide){
$provide.factory('dataSvc', ['$q', function($q)
function save(data){
if(passPromise){
return $q.when();
} else {
return $q.reject();
}
}
return{
save: save
};
}]);
});
然后,我们可以使用$rootScope.$new方法为控制器创建一个新的作用域。 创建控制器实例后,我们在此新$scope上拥有所有字段和方法。
beforeEach(inject(function($rootScope, $controller, dataSvc){
scope=$rootScope.$new();
mockDataSvc=dataSvc;
spyOn(mockDataSvc,'save').andCallThrough();
firstController = $controller('FirstController', {
$scope: scope,
dataSvc: mockDataSvc
});
}));
当控制器向$scope添加一个字段和方法时,我们可以检查它们是否设置为正确的值,以及这些方法是否具有正确的逻辑。 上面的示例控制器添加了一个正则表达式来检查有效数字。 让我们添加一个规范来测试正则表达式的行为:
it('should have assigned right pattern to numberPattern', function(){
expect(scope.numberPattern).toBeDefined();
expect(scope.numberPattern.test("100")).toBe(true);
expect(scope.numberPattern.test("100aa")).toBe(false);
});
如果控制器使用默认值初始化任何对象,我们可以在规范中检查它们的值。
要测试saveData方法,我们需要为bookDetails和bookForm对象设置一些值。 这些对象将绑定到UI元素,因此是在运行时在编译视图时创建的。 如前所述,我们需要在调用saveData方法之前使用一些值手动初始化它们。
以下代码段测试了此方法:
it('should call save method on dataSvc on calling saveData', function(){
scope.bookDetails = {
bookId: 1,
name: "Mastering Web application development using AngularJS",
author:"Peter and Pawel"
};
scope.bookForm = {
$setPristine: jasmine.createSpy('$setPristine')
};
passPromise = true;
scope.saveData();
scope.$digest();
expect(mockDataSvc.save).toHaveBeenCalled();
expect(scope.bookDetails).toEqual({});
expect(scope.bookForm.$setPristine).toHaveBeenCalled();
});
使用“ Controller as”语法测试控制器
测试使用Controller as语法的Controller as比使用$scope测试一个控制器容易。 在这种情况下,控制器的实例扮演模型的角色。 因此,该实例上的所有操作和对象均可用。
考虑以下控制器:
angular.module('controllers',[])
.controller('SecondController', function(dataSvc){
var vm=this;
vm.saveData = function () {
dataSvc.save(vm.bookDetails).then(function(result) {
vm.bookDetails = {};
vm.bookForm.$setPristine();
});
};
vm.numberPattern = /^\d*$/;
});
调用此控制器的过程类似于前面讨论的过程。 唯一的区别是,我们不需要创建$scope 。
beforeEach(inject(function($controller){
secondController = $controller('SecondController', {
dataSvc: mockDataSvc
});
}));
当控制器中的所有成员和方法都添加到该实例时,我们可以使用实例引用对其进行访问。
以下代码段测试了添加到上述控制器中的numberPattern字段:
it('should have set pattern to match numbers', function(){
expect(secondController.numberPattern).toBeDefined();
expect(secondController.numberPattern.test("100")).toBe(true);
expect(secondController.numberPattern.test("100aa")).toBe(false);
});
saveData方法的断言保持不变。 这种方法的唯一区别在于我们将值初始化为bookDetails和bookForm对象的方式。
以下代码段显示了规范:
it('should call save method on dataSvc on calling saveData', function ()
secondController.bookDetails = {
bookId: 1,
name: "Mastering Web application development using AngularJS",
author: "Peter and Pawel"
};
secondController.bookForm = {
$setPristine: jasmine.createSpy('$setPristine')
};
passPromise = true;
secondController.saveData();
rootScope.$digest();
expect(mockDataSvc.save).toHaveBeenCalled();
expect(secondController.bookDetails).toEqual({});
expect(secondController.bookForm.$setPristine).toHaveBeenCalled();
});
测试提供者
提供程序用于为应用程序范围的配置公开API,该API必须在应用程序启动之前进行。 AngularJS应用程序的配置阶段结束后,将不允许与提供程序进行交互。 因此,只能在配置块或其他提供程序块中访问提供程序。 我们无法使用注入模块获取提供程序实例,而是需要将回调传递给模块模块。
让我们考虑以下依赖于常量( appConstants )和第二个提供者( anotherProvider )的提供者:
angular.module('providers', [])
.provider('sample', function(appConstants, anotherProvider){
this.configureOptions = function(options){
if(options.allow){
anotherProvider.register(appConstants.ALLOW);
} else {
anotherProvider.register(appConstants.DENY);
}
};
this.$get = function(){};
});
为了测试这一点,我们首先需要模拟依赖项。 您可以在示例代码中看到如何执行此操作 。
在测试提供程序之前,我们需要确保模块已加载并准备就绪。 在测试中,将模块的加载推迟到执行注入模块或执行第一个测试之前。 在几个项目中,我看到了一些测试,这些测试使用空的第一个测试来加载模块。 我不喜欢这种方法,因为测试不会执行任何操作,并且会增加您的测试总数。 相反,我使用一个空的注入块来加载模块。
以下代码段获取引用并加载模块:
beforeEach(module("providers"));
beforeEach(function(){
module(function(anotherProvider, appConstants, sampleProvider){
anotherProviderObj=anotherProvider;
appConstantsObj=appConstants;
sampleProviderObj=sampleProvider;
});
});
beforeEach(inject());
现在我们有了所有引用,我们可以调用提供程序中定义的方法并对其进行测试:
it('should call register with allow', function(){
sampleProviderObj.configureOptions({allow:true});
expect(anotherProviderObj.register).toHaveBeenCalled();
expect(anotherProviderObj.register).toHaveBeenCalledWith(appConstantsObj.ALLOW);
});
结论
单元测试有时会变得很棘手,但是值得花些时间在上面,因为它可以确保应用程序的正确性。 AngularJS使对使用框架编写的代码进行单元测试变得更加容易。 我希望本文能给您足够的想法,以扩展和增强应用程序中的测试。 在以后的文章中,我们将继续研究如何测试您的其他代码。
From: https://www.sitepoint.com/unit-testing-angularjs-services-controllers-providers/
本文介绍如何使用Jasmine和Karma对AngularJS应用中的服务、控制器和提供程序进行单元测试。文章详细解释了模拟依赖项的重要性,并提供了具体示例。

136

被折叠的 条评论
为什么被折叠?



