Test setup is hard, it's tricky, and it feels like a hack. That's how you know you're doing it right. (maybe?)
- Why you aid testability by wrapping $resource in an api service.
- Test setup and mocking in jasmine to handle api services.
- How to test and resolve promises in your controller specs.
All of the code is available on plunkr
Dependency injection and single responsibility principle are crucial to testability. When you instantiate and create a $resource
object in your controllers you are being detrimental to the testability of that controller because you can no longer separate the two. Isolate the functionality of your controllers away from the grunt work of the services they use. The intent of controllers is to glue data models and ui methods to the $scope
. It's so easy with angular to do everything in your controllers, but you need to resist that urge and try to recognize when you have the opportunity to refactor a dependency injectable service into your design.
Instantiation and Tight Coupling:
angular
.module('BreakfastApp')
.controller(
'BreakfastCtrl',
function($scope) {
// ANTI-PATTERN !!!
var BagelResource = $resource('bagels.json');
}
);
Dependency Injection:
angular
.module('BreakfastApp')
.controller(
'BreakfastCtrl',
function(
$scope,
bagelApiService
) {
// bagelApiService is injected.
}
);
This is the controller we're testing:
script.js
// Breakfast App
angular.module('BreakfastApp', ['ngResource']);
// Bagel Api Service
angular.module('BreakfastApp').factory(
'bagelApiService',
function($resource) {
return $resource('bagels.json');
}
);
// Breakfast Controller
angular.module('BreakfastApp').controller(
'BreakfastCtrl',
function($scope, bagelApiService) {
bagelApiService
.query()
.$promise
.then(function(bagelsResponse) {
$scope.bagels = bagelsResponse;
$scope.somethingAfterBagelsLoad = true;
});
}
);
This is the entire example application. It consists of one controller which loads a collection of bagels via an "api" call (a flat bagels.json file for the sake of the example). The only other thing it does is set somethingAfterBagelsLoad
to true. This is a trivial thing to do after the bagels are loaded, and is again just for the purpose of exemplifying the use of promises and how to test them.
Okay, let's have a look at them specs.
specs.js
describe('BreakfastCtrl', function() {
var $q,
$rootScope,
$scope,
mockBagelApiService,
mockBagelsResponse;
beforeEach(module('BreakfastApp'));
beforeEach(inject(function(_$q_, _$rootScope_) {
$q = _$q_;
$rootScope = _$rootScope_;
}));
This loads the $q
and $rootScope
angular services. The inject
method can handle special dependencies wrapped on either side by _
as a convenience. This allows for assigning their values to local variables named appropriately.
I need $q
in order to build a promise I can return from the bagelApiService
. I need $rootScope
to both make a new scope for the BreakfastCtrl
and to propagate promise resolutions. $q
is integrated with $rootScope
, read about it in the $q documentation.
beforeEach(inject(function($controller) {
$scope = $rootScope.$new();
mockBagelApiService = {
query: function() {
queryDeferred = $q.defer();
return {$promise: queryDeferred.promise};
}
}
spyOn(mockBagelApiService, 'query').andCallThrough();
$controller('BreakfastCtrl', {
'$scope': $scope,
'bagelApiService': mockBagelApiService
});
}));
This block builds the dependencies for the controller, and constructs it via the $controller
service. This alters the controller's understanding of what $scope
and bagelApiService
are, and replaces them with objects made locally. If you're not familiar with unit testing, or its purpose, this can be a confusing thing to do. Why are we doing this? Why make fake objects to trick the controller? The answer is pretty simple: to isolate the subject of the test and write reliable assertions.
For instance, the mockBagelsResponse
we made at the top of the file is a set of predictable values used in the tests below to make assertions. With the mock for the bagelApiService
in place, those predictable values will be used to resolve the queryDeferred.promise
used by mockBagelApiService.query
without ever actually running any code from the real bagelApiService
or sending any real requests to the "api".
describe('bagelApiService.query', function() {
beforeEach(function() {
queryDeferred.resolve(mockBagelsResponse);
$rootScope.$apply();
});`
Resolve the bagelApiService.query
method's promise with a fake, but predictable mockBagelResponse
array. Propagate the resolution with $rootScope.$apply()
.
it('should query the bagelApiService', function() {
expect(mockBagelApiService.query).toHaveBeenCalled();
});
it('should set the response from the bagelApiServiceQuery to $scope.bagels', function() {
expect($scope.bagels).toEqual(mockBagelsResponse);
});
it('should set $scope.somethingAfterBagelsLoad to true', function() {
expect($scope.somethingAfterBagelsLoad).toBe(true);
});
});
Those tests end up pretty simple, which is what you want. The service was called, the $scope.bagels
are set, and $scope.somethingAfterBagelsLoad
happened.
I sincerely hope this helps you figure out how to test your angular code. The usefulness of promises comes up a lot, and there aren't enough in depth examples of how to write angular with $q
and even fewer examples of how to test it. At Cascade Energy, much of our client code utilizes the promise api, and as a result I have a fairly large amount of experience working with and testing it. Please feel free to reach out with questions here on the blog or on twitter @nackjicholsonn if you have concerns that go beyond the bounds of this simplified example. Cheers, thanks for reading.
Again, run, fork, and play with this plunkr
Hi Nackjicholson, flawless information. As a matter of fact, i have a small clarification where i am struck on .
The isolated "resource factory(ie bagelServiceAPI) returns the instantiated "resource Object" for a static url (Particularly, only one request in your example), So, hence, it is possible to replace the original "Service API" with that of the "locally" created one in the test (ie. mockBagelServiceAPI), thus, mocking the implementation of ".query()" within the test.... Which is great actually, but only of a single request.
Now, my question is, what if there are multiple "http request" that needs to be done through "$resource Object for different URLs, to be specific", which eventually makes use of the same "Serivce API (ie, bagerServiceAPI)..?
In Other words, i just want my "Service API" to be reusable , so that "resource objects " can be instantiated and acquired for different "URLs"..
Would really appreciate if you could help me out with this, (will admire , if suitable examples provided along)..
Thank you.