Javascript AngularJS中的单元测试-模拟服务和承诺
在Angular中,一切似乎都有一个陡峭的学习曲线,Angular应用程序的单元测试肯定无法逃脱这种范式 当我开始使用TDD和Angular时,我觉得我花了两倍(可能更多)的时间来弄清楚如何测试,甚至更多的时间来正确设置测试。但正如他在博客中所说,学习过程中有起有落。他的图表绝对是我在角度方面的经验 然而,随着我在学习角度测试和单元测试方面的进步,现在我觉得我花更少的时间设置测试,花更多的时间让测试从红色变为绿色,这是一种很好的感觉 因此,我遇到了设置模拟服务和承诺的单元测试的不同方法,我想我会分享我学到的知识,并提出以下问题: 有没有其他或更好的方法来实现这一点 所以在代码上,不管怎么说,这就是我们来到这里的目的——不是听一些人谈论他的爱,呃,学习框架的成就 这就是我如何开始嘲笑我的服务和承诺的,我将使用控制器,但服务和承诺显然可以在其他地方被嘲笑Javascript AngularJS中的单元测试-模拟服务和承诺,javascript,angularjs,unit-testing,testing,jasmine,Javascript,Angularjs,Unit Testing,Testing,Jasmine,在Angular中,一切似乎都有一个陡峭的学习曲线,Angular应用程序的单元测试肯定无法逃脱这种范式 当我开始使用TDD和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
如果我们想重新初始化控制器,那么我们必须将其从beforeach块中拉出,并将其单独注入到每个测试中
一定有更好的方法
现在我问是否有人有其他方法来设置测试,或者我选择的方式有什么想法或感觉?然后我看到另一篇文章,博客,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
语句,因此不太清楚。此外,将产品
的模拟传递到$controller
函数本身似乎很不寻常,这意味着您可能有两种不同的产品
服务可用。一个直接注入控制器,另一个由普通角度DI系统注入其他服务。我认为最好使用$provide
来注入mock,然后Angular中的每个地方都会有相同的实例用于任何测试
综上所述,下面这样的内容似乎更好,可以在
请注意,缺少if
语句,并且getProductsDerred
对象和getProducts
mock的限制限制在Descripte
块的范围内。使用这种模式,意味着您可以在产品
的其他方法上添加其他测试,而不会污染模拟产品
对象或您拥有的设置控制器
功能,以及测试所需的所有可能方法/组合
作为侧边栏,我注意到:
module('App.Controllers.Products');
module('App.Services.Products');
意味着您将控制器和服务分离为不同的角度模块。我知道某些博客推荐了这一点,但我怀疑这太复杂了,每个应用程序一个模块就可以了。如果您随后进行重构,并使服务和指令完全分离可重用组件,那么就应该将它们放在单独的模块中,并像使用任何其他第三方模块一样使用它们
编辑:将
$provide.provide
更正为$provide.value
,并修复了控制器/服务实例化的一些顺序,并添加了指向Plunkr的链接,感谢Michal花时间查看。然而,我认为您可能已经错过了实现的完整画面。我没有通过2个产品和服务。一个是全局变量,因此我们可以传递它-产品,另一个产品是从承诺返回的实际产品。最后,单词product的第三个用法是setupController fct中的一个参数,它只确定我要返回哪个产品,布尔值确定承诺是拒绝还是解决了-需要测试这两个实例。我将重命名一些变量并添加更多注释我们需要单独的模块,因为对于单元测试来说,这是理想的-如果你不理解为什么我可以更详细地讨论。但归根结底,在不相关模块的单元测试中,不需要其他模块的功能。是的,我们正在使模块与可重用组件分离。我喜欢resolve和reject的实现,但是如果我需要使用不同的设置测试控制器,该怎么办?使用您的方法,我只能使用一个控制器设置。但是,使用函数setupController,我可以选择我希望控制器的设置方式,它使代码更加简洁。我知道您没有传递2个服务,但我只想说,在测试期间使用$provide
注入服务模拟更为常见(据我所知)。我已将一个变量重命名为PRODUCTS[0]
,以便与您的名称更加一致。我意识到我的代码不会像您所希望的那样工作,并在response和bit.ly/OIPZoQ中进行了修复。我现在同意你仍然需要一个createController函数,我已经编辑了我的答案,包括一个:但重要的一点是它除了创建一个控制器之外什么都不做
module('App.Controllers.Products');
module('App.Services.Products');