AngularJS 中的单元测试 - 模拟服务和承诺
Unit testing in AngularJS - Mocking Services and Promises
中,一切似乎都有一个陡峭的学习曲线,对Angular应用程序进行单元测试绝对无法逃脱这种范式。
当我开始使用TDD和Angular时,我觉得我花了两倍(也许更多)的时间弄清楚如何测试,甚至可能更多的时间只是正确设置我的测试。但正如本·纳德尔(Ben Nadel)在他的博客中所说的那样,角度学习过程中有起有落。他的图表绝对是我对Angular的经验。
然而,随着我在学习 Angular 和单元测试方面也取得了进步,现在我觉得我花在设置测试上的时间少得多,而花在让测试从红色变成绿色上的时间要多得多——这是一种很好的感觉。
因此,我遇到了设置单元测试以模拟服务和承诺的不同方法,我想我会分享我所学到的知识,并提出以下问题:
还有其他或更好的方法可以做到这一点吗?
所以在代码上,无论如何我们都来这里的目的 - 不是听某个人谈论他的爱,错误地学习一个框架。
这就是我开始嘲笑我的服务和承诺的方式,我将使用控制器,但服务和承诺显然可以在其他地方被嘲笑。
describe('Controller: Products', function () {
var//iable declarations
$scope,
$rootScope,
ProductsMock = {
getProducts: function () {
} // There might be other methods as well but I'll stick to one for the sake of consiseness
},
PRODUCTS = [{},{},{}]
;
beforeEach(function () {
module('App.Controllers.Products');
});
beforeEach(inject(function ($controller, _$rootScope_) {
//Set up our mocked promise
var promise = { then: jasmine.createSpy() };
//Set up our scope
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
//Set up our spies
spyOn(ProductsMock, 'getProducts').andReturn(promise);
//Initialize the controller
$controller('ProductsController', {
$scope: $scope,
Products: ProductsMock
});
//Resolve the promise
promise.then.mostRecentCall.args[0](PRODUCTS);
}));
describe('Some Functionality', function () {
it('should do some stuff', function () {
expect('Stuff to happen');
});
});
});
对我们来说,这奏效了,但随着时间的推移,我认为一定有更好的方法。对于一个我讨厌
promise.then.mostRecentCall
如果我们想重新初始化控制器,那么我们必须将其从 beforeEach 块中拉出,并将其单独注入到每个测试中。
必须有更好的方法...
现在我问是否有人有其他方法来设置测试,或者对我选择的方式有想法或感觉?
然后我遇到了另一篇文章,博客,stackoverflow示例(你选择它我可能在那里),我看到了$q库的使用。咄!当我们可以使用 Angular 给我们的工具时,为什么要设置一个完整的模拟承诺。我们的代码看起来更好,看起来更有意义 - 没有丑陋的承诺。
单元测试迭代的下一个是这样的:
describe('Controller: Products', function () {
var//iable declarations
$scope,
$rootScope,
$q,
$controller,
productService,
PROMISE = {
resolve: true,
reject: false
},
PRODUCTS = [{},{},{}] //constant for the products that are returned by the service
;
beforeEach(function () {
module('App.Controllers.Products');
module('App.Services.Products');
});
beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
$rootScope = _$rootScope_;
$q = _$q_;
$controller = _$controller_;
productService = _products_;
$scope = $rootScope.$new();
}));
function setupController(product, resolve) {
//Need a function so we can setup different instances of the controller
var getProducts = $q.defer();
//Set up our spies
spyOn(products, 'getProducts').andReturn(getProducts.promise);
//Initialise the controller
$controller('ProductsController', {
$scope: $scope,
products: productService
});
// Use $scope.$apply() to get the promise to resolve on nextTick().
// Angular only resolves promises following a digest cycle,
// so we manually fire one off to get the promise to resolve.
if(resolve) {
$scope.$apply(function() {
getProducts.resolve();
});
} else {
$scope.$apply(function() {
getProducts.reject();
});
}
}
describe('Resolving and Rejecting the Promise', function () {
it('should return the first PRODUCT when the promise is resolved', function () {
setupController(PRODUCTS[0], PROMISE.resolve); // Set up our controller to return the first product and resolve the promise.
expect('to return the first PRODUCT when the promise is resolved');
});
it('should return nothing when the promise is rejected', function () {
setupController(PRODUCTS[0], PROMISE.reject); // Set up our controller to return first product, but not to resolve the promise.
expect('to return nothing when the promise is rejected');
});
});
});
这开始感觉像是它应该设置的方式。我们可以嘲笑我们需要嘲笑的东西,我们可以设定我们的承诺来解决和拒绝,这样我们就可以真正测试两种可能的结果。这感觉很好...
你自己回答中关于使用$q.defer
的要点听起来不错。我唯一的补充是
setupController(0, true)
不是特别清楚,由于参数 0
和 true
,然后是使用它的 if
语句。此外,将 products
的模拟传递到 $controller
函数本身似乎不寻常,这意味着您可能有 2 种不同的products
服务可用。一个直接注入控制器,一个由通常的 Angular DI 系统注入其他服务。我认为最好使用 $provide
来注入模拟,然后 Angular 中的任何地方对于任何测试都有相同的实例。
综上所述,下面这样的东西似乎更好,可以在 http://plnkr.co/edit/p676TYnAIb9QlD7MPIHu?p=preview
describe('Controller: ProductsController', function() {
var PRODUCTS, productsMock, $rootScope, $controller, $q;
beforeEach(module('plunker'));
beforeEach(module(function($provide){
PRODUCTS = [{},{},{}];
productsMock = {};
$provide.value('products', productsMock);
}));
beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
$rootScope = _$rootScope_;
$q = _$q_;
$controller = _$controller_;
products = _products_;
}));
var createController = function() {
return $controller('ProductsController', {
$scope: $rootScope
})
};
describe('on init', function() {
var getProductsDeferred;
var resolve = function(results) {
getProductsDeferred.resolve(results);
$rootScope.$apply();
}
var reject = function(reason) {
getProductsDeferred.reject(reason);
$rootScope.$apply();
}
beforeEach(function() {
getProductsDeferred = $q.defer();
productsMock.getProducts = function() {
return getProductsDeferred.promise;
};
createController();
});
it('should set success to be true if resolved with product', function() {
resolve(PRODUCTS[0]);
expect($rootScope.success).toBe(true);
});
it('should set success to be false if rejected', function() {
reject();
expect($rootScope.success).toBe(false);
});
});
});
请注意,缺少if
语句,以及将getProductsDeferred
对象限制在describe
块的作用域getProducts
模拟。使用这种模式,意味着您可以在其他products
方法上添加其他测试,而不会污染模拟products
对象或您拥有的setupController
函数,以及测试所需的所有可能方法/组合。
作为侧边栏,我注意到:
module('App.Controllers.Products');
module('App.Services.Products');
意味着您将控制器和服务分离到不同的 Angular 模块中。我知道某些博客推荐了这个,但我怀疑这过于复杂了,每个应用程序一个模块是可以的。如果你然后重构,并使服务和指令完全分离可重用的组件,那么是时候将它们放入一个单独的模块中,并像使用任何其他第三方模块一样使用它们。
编辑:更正了$provide.value
$provide.provide
,并修复了控制器/服务实例化的一些顺序,并添加了指向Plunkr的链接
- 棱角分明的服务承诺
- 角度控制器服务承诺不起作用
- 服务承诺不以业力解决
- 承诺返回未定义的服务
- angular服务中的$http承诺
- 如何在两个连续的角度服务呼叫期间中止/取消第一个呼叫的承诺
- 服务中的承诺会自行取消
- 为根范围承诺提供角度服务
- 使用 Jasmine 2.4.* 测试返回承诺的服务
- 角度“this”符号和来自服务的数据,带有承诺
- AngularJS,在服务中使用承诺
- 角度控制器检查服务承诺更新视图
- 在解决承诺 AngularJS/ES6 后检索存储在服务中的数据
- 如何在返回承诺的服务中创建 reset() 方法
- AngularJS$http服务中的调用,返回解析的数据,而不是承诺
- 测试承诺与柴和西农的服务
- Angular 1的服务承诺
- 服务承诺在茉莉花中没有得到解决
- Angularjs单元测试服务承诺
- Angular服务承诺