ngImprovedTesting: mock testing for AngularJS made easy
Being able to easily test your application is one of the most powerful features that AngularJS offers. All the services, controllers, filters even directives you develop can be fully (unit) tested.
However the learning curve for writing (proper) unit tests tends to be quite steep.
This is mainly because AngularJS doesn’t really offer any high level API’s to ease the unit testing. Instead you are forced to use the same (low level) services that AngularJS uses internally. That means you have to gain in dept knowledge about the internals of $controller, when to $digest and how to use $provide in order to mock these services. Especially mocking out a dependency of controller, filter or another service is too cumbersome.
This blog will show how you would normally create mocks in AngularJS, why its troublesome and finally introduces the new ngImprovedTesting library that makes mock testing much easier.
Sample application
Consider the following application consisting of the “userService” and the “permissionService”:
var appModule = angular.module('myApp', []); appModule.factory('userService', function($http) { var detailsPerUsername = {}; $http({method: 'GET', url: '/users'}) .success(function(users) { detailsPerUsername = _.indexBy(users, 'username'); }); return { getUserDetails: function(userName) { return detailsPerUsername[userName]; } }; }); appModule.factory('permissionService', function(users) { return { hasAdminAccess: function(username) { return users.getUserDetails(username).admin === true; } }; });
When it comes to unit testing “permissionService” there are two default strategies:
- using mock $httpBackend (from the ngMock module) to simulate $http trafic from the “userService”
- using a mock instead of the actual “userService” dependency
Replacing the “userService” with a mock using vanilla AngularJS
Using vanilla AngularJS you have to do all the hard work yourself when you like to create a mock. You will have to manually create an object with its relevant fields and methods. Finally you will have to register the mock (using $provide) to overwrite the existing service implementation.
Using the following vanilla AngularJS we can replace “userService” with a mock in our unit tests:
describe('Vanilla mocked style permissions service specification', function() { var userServiceMock; beforeEach(module('myApp', function ($provide) { userServiceMock = { getUserDetails: jasmine.createSpy() }; $provide.value('userService', userServiceMock); })); // ...
The imperfections of the vanilla style of mocking
To ability to mock services in unit tests is a really great feature in AngularJS but it’s far from perfect.
As a developer I really don’t want to be bothered with having to manually create a mock object. For instance I might just simply forget to mock the “userService” dependency when testing the “permissionService” meaning I would accidentally test it using the actual “userService”. And what if you would refactor the “userService” and would rename its method to “getUserInfo”.
Then you would except the unit test of “permissionService” to fail, right? But it won’t since the mocked “userService” still has the old “getUserDetails” (spy) method.
Make things even worse… what if you would rename service to “userInfoService”. This makes the “userService” dependency of the “permissionService” to be no longer resolvable. Due to this modification the application will no longer bootstrap when executed inside a browser. But when executed from the unit test it won’t fail since its still uses its own mock. However other unit tests using the same module but not mocking the service will fail.
How mock testing could be improved
Coming from a Java background if found the manual creation of mocks felt quite weird to me. In static languages the existence of interfaces (and classes) make it way more easy to automatically create mocks.
Using AngularJS we could do something similar …
… what if we would use the original service as a template for creating a mocked version.
Then we could automatically create mocks that contain the same properties as the original object. Each non-method property could be copied as-is and each method would instead be a Jasmine spy.
Instead of manually registering a mock service using $provide we could instead automate this. This would also allow us to automatically check if a service you want to mock actually exists. Also we could check if the service being mock is indeed being used as dependency of a component.
Introducing the ngImprovedTesting library
With the intention of making (unit) testing more easy I created the “ngImprovedTesting” library. The just released 0.1 version supports (selectively) mocking out dependencies of a controller, filter or another service.
Mock out the “userService” dependency when testing the “permissionService” is now extremely easy:
describe('ngImprovedTesting mocked style permissions service specification', function() { beforeEach(ModuleBuilder.forModule('myApp') .serviceWithMocksFor('permissionService', 'userService') .build()); // ... continous in next code snippets
Instead of using the traditional “beforeEach(module(‘myApp’))” we are using the ModuleBuilder of “ngImprovedTesting” to build a module specifically for our test. In this case we would like to test the actual “permissionService” in a test in combination with a mock for its “userService” dependency.
But what if I would like to set some behavior on the automatically created mock …
… how do I actually get a hold on the actual mock instance?
Well simple… besides the component being tested all its dependencies including the mocked one can be injected.
To differentiate a mock from a regular one it’s registered with “Mock” appended in its name. So to inject the mocked out version of “userService” just use “userServiceMock” instead:
describe('hasAdminAccess method', function() { it('should return true when user details has property: admin == true', inject(function(permissions, userServiceMock) { userServiceMock.getUserDetails.andReturn({admin: true}); expect(permissions.hasAdminAccess('anAdminUser')).toBe(true); })); });
As you can see in the example the “userServiceMock.getUserDetails” method is a just a Jasmine spy. It therefor allows invocation of “andReturn” on in order to set the return value of the method. However it does not allow an “andCallThrough” as the spy is not on the original service.
Exploring the ModuleBuilder API of ngImprovedTesting
Since I didn’t get round to writing and generating JSDocs / NGDocs, I instead will quickly explain it here.
To instantiate a “ModuleBuilder” use its static “forModule” method.
The “ModuleBuilder” (in version 0.1) consists of the following instance methods:
- serviceWithMocksFor: registers a service for testing and mock specified dependencies
- serviceWithMocks: registers a service for testing and mock all dependencies
- serviceWithMocksExcept: registers a service for testing and mock dependencies except the specified
- controllerWithMocksFor: registers a controller for testing and mock specified dependencies
- controllerWithMocks: registers a controller for testing and mock all dependencies
- controllerWithMocksExcept: registers a controller for testing and mock dependencies except the specified
- controllerAsIs: registers a controller so that it can be instantiated through $controller
- filterWithMocksFor: registers a filter for testing and mock specified dependencies
- filterWithMocks: registers a filter for testing and mock all dependencies
- filterWithMocksExcept: registers a filter for testing and mock dependencies except the specified
- filterAsIs: registers a filter so that is can be using through $filter
Limitations in the initial (0.1) of ngImprovedTesting
Although version 0.1 is quite production ready (and well unit tested) is has its limitations:
- Services registered with the “provider” method currently cannot be used as to be tested service; meaning it cannot be used as first parameter of “serviceWithMocks…”, however it can be used as a (potentially mocked) dependency.
- Services which are registered using “$provide” (i.e. inside a config function of a module) instead of through “angular.Module” cannot be used as to be tested service.
- Mock testing of directives is currently not supported.
How to get started with ngImprovedTesting
All sources from this blog post can be found as part of a sample application:
The sample applications demonstrates three different flavors of testing:
- One that uses the $httpBackend
- Another using vanilla mocking support
- And one using ngImprovedTesting
To execute the tests on the command-line use the following commands (requires NodeJS, NPM, Bower and Grunt to be installed):
npm install bower update grunt
The actual sources of ngImprovedTesting itself are also hosted on GitHub:
- https://github.com/evangalen/ng-improved-testing.git: contains the source code on ngImprovedTesting itself.
- https://github.com/evangalen/ng-module-introspector.git: specifically developed AngularJS module introspector that allows us to retrieve the exact declaration of a controller, filter and service and its dependencies.
Furthermore ngImprovedTesting is also available through bower itself. You can easily install and add it to an existing project using the following command:
bower install ng-improved-testing --save-dev
Your feedback is more than welcome
My goal for ngImprovedTesting is to ease mock testing in your AngularJS unit tests.
I’m very interested in your feedback… is ngImprovedTesting any useful… and how could it be improved?
Reference: | ngImprovedTesting: mock testing for AngularJS made easy from our JCG partner Emil van Galen at the JDriven blog. |