In Angular everything seems to have a steep learning curve and unit testing an Angular app definitely doesn\'t escape this paradigm.
When I started with TDD and Ang
The main point in your own answer about using $q.defer
sounds good. My only additions would be that
setupController(0, true)
is not particularly clear, due to the parameters 0
and true
, and then the if
statement that uses this. Also, passing the mock of products
into the $controller
function itself seems unusual, and means you might have 2 different products
services available. One directly injected into the controller, and one injected by the usual Angular DI system into other services. I think better to use $provide
to inject mocks and then everywhere in Angular will have the same instance for any test.
Putting this all together, something like the following seems better, which can be seen at 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);
});
});
});
Notice that lack of if
statement, and the limitation of the getProductsDeferred
object, and getProducts
mock, to the scope of a describe
block. Using this sort of pattern, means you can add other tests, on other methods of products
, without polluting the mock products
object, or the setupController
function you have, with all the possible methods / combinations you need for the tests.
As a sidebar, I notice:
module('App.Controllers.Products');
module('App.Services.Products');
means you are separating your controllers and services into different Angular modules. I know certain blogs have recommended this, but I suspect this overcomplicated things, and a single module per app is ok. If you then refactor, and make services and directives completely separate reusable components, then it would be time to put them into a separate module, and use them as you would any other 3rd party module.
Edit: Corrected $provide.provide
to $provide.value
, and fixed some of the ordering of instantiation of controller/services, and added a link to Plunkr