quickmock is a micro-library for initializing, mocking and auto-injecting provider dependencies for Jasmine/Mocha/Chai/Sinon unit tests in AngularJS. View an introduction video by clicking here.
Mocking out dependencies in unit tests can be a huge pain. Angular makes testing "easy", but mocking out every dependecy isn't so slick. If you've ever written an Angular unit test (using Jasmine/Mocha), you've probably seen a ton of beforeEach
boilerplate that looks something like this:
describe('zb-toggle Directive', function () {
var scope, element, notificationService, $compile;
beforeEach(function(){
module('AppModule');
module(function($provide){
var mockNotificationService = jasmine.createSpyObj('NotificationService',
['error','success','warning','basic','confirm']);
$provide.value('NotificationService', mockNotificationService);
});
inject(function(_$rootScope_, _$compile_, _NotificationService_){
scope = _$rootScope_.$new();
$compile = _$compile_;
notificationService = _NotificationService_;
});
});
beforeEach(function(){
element = angular.element('<div zb-toggle></div>');
$compile(element)(scope);
scope.$digest();
});
// ... write actual test cases here
});
The module containing the directive must first be initialized. Then any dependencies must be mocked out and "provided" to the Angular injector. Then $rootScope
, $compile
and the mocked service must be injected into your testing environment to be referenced later. Then you have to render, compile and $digest()
the directive HTML. Once all that is done, you can begin writing test cases.
What if it was a lot easier? What if we could reduce all of that down to this?
describe('zb-toggle Directive', function () {
var zbToggle;
beforeEach(function(){
zbToggle = quickmock({
providerName: 'zbToggle',
moduleName: 'QuickMockDemo',
mockModules: ['QuickMockDemoMocks'],
html: '<div zb-toggle></div>'
});
zbToggle.$compile();
});
// ... write actual test cases here
});
quickmock does all of that beforeEach
boilerplate behind the scenes, and returns an object that contains all of the data you need to write your tests. Mocks are defined in their own reusable Angular modules. quickmock then sees which dependencies your provider (i.e. service/factory/directive/filter/controller/etc) has, looks up the mocks for those dependencies, injects them into the provider and into your test, and finally bootstraps all the required modules.
Let's start with a simple example. quickmock can work with even the most complex providers, but to start out, we'll choose an easy service that we want to test. Let's say we have the following 'NotificationService'
provider:
angular.module('QuickMockDemo', [])
.service('NotificationService', ['$window', 'NotificationTitles', function($window, titles){
return {
error: function notificationError(msg){
$window.alert(titles.error + '\n\n' + msg);
},
success: function notificationSuccess(msg){
$window.alert(titles.success + '\n\n' + msg);
},
warning: function notificationWarning(msg){
$window.alert(titles.warning + '\n\n' + msg);
},
basic: function notificationBasic(msg){
$window.alert(titles.basic + '\n\n' + msg);
},
confirm: function notificationConfirm(msg){
return $window.confirm(titles.confirm + '\n\n' + msg);
}
};
}])
Its a simple service that shows various alert messages to the user, by delegating to window.alert()
. It has two dependencies: $window
and NotificationTitles
, which is looks like this:
.value('NotificationTitles', {
error: 'It looks like something went wrong...',
success: 'Congraduations!',
warning: 'Be careful...',
basic: 'Check this out!',
confirm: 'Confirm Action'
})
We can test this service by writing the following:
describe('NotificationService', function () {
var notificationService;
beforeEach(function(){
notificationService = quickmock({
providerName: 'NotificationService', // the provider we wish to test
moduleName: 'QuickMockDemo', // the module that contains our provider
mockModules: ['QuickMockDemoMocks'] // module(s) that contains mocks for our provider's dependencies
});
});
....
quickmock will find the NotificationService
and lookup its list of dependencies. It will then try to find mocks for each of those dependencies and inject them into your test.
You can provide mocks for each of the dependencies by creating a separate javascript file and writing a separate Angular module to contain those mocks. This provides several benefits: it allows your mocks to be reusable between tests, gives you a specific structure for writing your tests, and easily integrates with quickmock.
quickmock provides a simple syntax for declaring mocks in a module found in quickmock.mockHelper.js
. It allows you to declare mocks as seen in the following simple example:
// Declare an Angular module that will contain any mocks you need
angular.module('SampleMocks', [])
// now declare specific mocks for each of your dependencies
.mockService('NotificationService', [function(){
return jasmine.createSpyObj('NotificationService', ['error','success','warning','basic','confirm']);
}])
.mockFactory('UserFormValidator', [function(){
var spy = jasmine.createSpy('UserFormValidator');
spy.and.returnValue(true);
return spy;
}])
A detailed explanation of the two possible mock declaration syntaxes (and their advantages and disadvantages) can be found here.
For further information about how to write specific mocks to acurately mock out your providers, see this SitePoint article or the Angular Developer Guide: Unit Testing.
As shown in the example above, a call to quickmock accepts a config object and returns an object, which in this case we called notificationService
.
providerName
(String) - The name of the provider you want to testmoduleName
(String) - The name of the module that contains the provider abovemockModules
(Array:String) - An array of the names of modules that contain mocks for any of the provider's dependenciesuseActualDependencies
(Boolean) - If quickmock cannot find a mock for a required dependency, it will thrown an exception. If, instead, you wish to delegate to the actual implementations of the dependencies instead of mocking them out, set this flag to true
.spyOnProviderMethods
(Boolean) - If true, quickmock will automatically spy on the methods of the provider. This will give you access to all of the usual spy functionality for any methods on your provider, but will also call through to the actual implementation so you can test all required functionality. This is very useful when testing certain provider methods that call one another.html
(String|Object) - For directives only, this is the default html that will be compiled when .$compile()
is called (this is explained below).inject
(Function) - An injection callback that will return instantiated angular providers. Similar to Angular's global inject(...)
method. You can see an example here. Using the Angular version will not work with quickmock. If you need to inject services/providers, you must use this method. The notificationService
object provides all of the data you need to write tests for the NotificationService
provider. Here is a walk through of all the information you are given. The following properties are avaiable when testing all provider types (services/factories/directives/controllers/filters/etc):
.$mocks
(Object) - contains all of the mocked dependencies for the provider you are testing// to test the NotificationService.error() method
it('should display proper error message to the user', function(){
notificationService.error('some fake message'); // calls to the provider
var mock_$window = notificationService.$mocks.$window,
mock_NotificationTitles = notificationService.$mocks.NotificationTitles;
var messageShown = mock_$window.alert.calls.argsFor(0)[0];
expect(messageShown).toContain(mock_NotificationTitles.error);
expect(messageShown).toContain('some fake message');
});
.$initialize()
(Function) - calling .$initialize() will re-initialize the provider you are testing. This is useful if you are testing any functionality that happens at the moment your service is initialized.The following properties are specific to testing directive
providers and will reference the following example directive:
.directive('zbToggle', ['NotificationService', function(NotificationService){
return {
restrict: 'AE',
replace: true,
transclude: true,
template: '<div class="toggle" ng-click="check = !check">'
+ '<input type="checkbox" ng-model="check">'
+ '<span ng-transclude></span>'
+ '</div>',
scope: {
initState: '='
},
link: function(scope, elem, attrs){
var notificationMessage = 'Your preference has been set to: ';
scope.check = scope.initState || false;
scope.$watch('check', function(val){
NotificationService[val ? 'success' : 'warning'](notificationMessage + val);
});
}
};
}])
.$compile([html])
(Function) - compiles the given html string/object and calls then $scope.$digest()
. If no html string/object is given, it will default to the html provided in the config object. You can provide a javascript object, which will be generated into an html string.it('should compile the given html string', function(){
zbToggle.$compile('<div zb-toggle class="btn btn-round" init-state="true"></div>');
expect(zbToggle.$element[0].tagName).toEqual('DIV');
expect(zbToggle.$element.hasClass('btn-round')).toEqual(true);
expect(zbToggle.$scope.initState).toEqual(true);
zbToggle.$compile('<span zb-toggle class="btn btn-shadow" init-state="false"></span>');
expect(zbToggle.$element[0].tagName).toEqual('SPAN');
expect(zbToggle.$element.hasClass('btn-shadow')).toEqual(true);
expect(zbToggle.$scope.initState).toEqual(false);
});
it('should compile the given html object', function(){
var htmlObj = {
$tag: 'div', // $tag (required): will be the html tagName (i.e. '<zb-toggle ...>' or '<div ...>' or '<span ...>')
$content: '', // $content (optional): will be inner content of the html element
zbToggle: '',
class: 'btn btn-round',
initState: true // properties are normalized (i.e. 'initState: true' will become 'init-state="true"')
};
zbToggle.$compile(htmlObj);
expect(zbToggle.$element[0].tagName).toEqual('DIV');
expect(zbToggle.$element.hasClass('btn-round')).toEqual(true);
expect(zbToggle.$scope.initState).toEqual(true);
htmlObj.$tag = 'span';
htmlObj.class = 'btn btn-shadow';
htmlObj.initState = false;
zbToggle.$compile(htmlObj);
expect(zbToggle.$element[0].tagName).toEqual('SPAN');
expect(zbToggle.$element.hasClass('btn-shadow')).toEqual(true);
expect(zbToggle.$scope.initState).toEqual(false);
});
.$element
(jQuery/jqLite Element) - provides access to the element that results from angular.element()
.it('should have the toggle class', function(){
expect(zbToggle.$element.hasClass('toggle')).toBe(true);
});
it('should show a checkbox', function(){
var input = zbToggle.$element.find('input');
expect(input.attr('type')).toEqual('checkbox');
});
.$scope
(Object) - provides access to the directive's Angular scope
objectit('should toggle the checkbox when clicked', function(){
expect(zbToggle.$scope.check).toBe(false);
zbToggle.$element[0].click();
expect(zbToggle.$scope.check).toBe(true);
zbToggle.$element[0].click();
expect(zbToggle.$scope.check).toBe(false);
});
it('should show a success message when toggled to true', function(){
expect(zbToggle.$scope.check).toBe(false);
zbToggle.$scope.check = true;
zbToggle.$scope.$digest();
expect(zbToggle.$mocks.NotificationService.success).toHaveBeenCalled();
});
.$isoScope
(Object) - provides access to the directive's Angular isoloateScope
object, if one existsit('should have an isolateScope', function(){
expect(zbToggle.$isoScope).toBe(zbToggle.$element.isolateScope());
});
The examples above are very simple. You will find more in-depth examples for each of the various provider types in the demo/app.js
file. Each of the providers in that file have their own quickmock test files that give more details on how to test them using quickmock. Each of these specs files are found in the specsUsingQuickMock
folder.
Type | Name | Spec File |
---|---|---|
service |
APIService |
apiService.spec.js |
factory |
UserFormValidator |
userFormValidatorService.spec.js |
service |
NotificationService |
notificationService.spec.js |
controller |
FormController |
formController.spec.js |
directive |
zb-toggle |
zbToggleDirective.spec.js |
filter |
firstInitialLastName |
firstInitialLastNameFilter.spec.js |
For those who are curious, there are also examples of testing these same providers without using quickmock (for comparison). These specs are found in the specsWithoutUsingQuickMock
folder.
You will also find example mocks for each of these providers, as well as mocks the angular $promise
, $http
and $scope
services in the mocks
folder.
You might find that quickmock logs a fair amount of information to the console. This is simply to make sure that you know what is happening behind the scenes and are aware of any possible warnings that might pop up. If you wish to turn off logging, you can simply set the quickmock.MUTE_LOGS
flag to true
. This will disable the logs and you won't see any data from quickmock output to the console. This DOES NOT, however, turn off exceptions that may be thrown as a result of required parameters not being available, such as angular
being undefined
or if you're missing required config parameters.
quickmock will throw exceptions if the provider you are trying to test has a dependency that does not have a mock registered for it. This exception is thrown so that you don't accidentally execute a test using the actual implementation of a dependency when you meant to have provided a mock for it. However, there are certainly times where you might want to use the actual implementation of a provider instead of its mock. For example, if your provider depends on $http
, you may want to use the mock for $http
provided by angular's ngMock
module instead of providing your own mock. There are three ways to handle this scenario:
1 useActualDependencies: true
- This tells quickmock to always default to using the actual implemenations of providers whenever it can't find its associated mock. Be careful using this flag because it can be easy to assume you are using mocks for all of your dependencies when in reality you aren't. If you accidentally forget to provide a mock for any other dependencies, quickmock will use the implementation and you won't know any different.
quickmock({
providerName: '...',
moduleName: '...',
useActualDependencies: true
});
2 mocks: {...}
- This config object will override any mock provided by any other module. You can either provide a mock for a provider, or set a given dependency to quickmock.USE_ACTUAL
, which will use the actual implementaiton of that specific mock, but still require mocks to be provided for all other dependencies.
quickmock({
providerName: '...',
moduleName: '...',
mocks: {
$http: quickmock.USE_ACTUAL, // uses actual implementation of $http
MyOtherDependency: 'someStringValue' // uses "someStringValue" instead of mock for MyOtherDependency
}
});
3 Register a wrapper mock - Simply register a wrapped mock for the given dependency. This is a little tedious, but if you are constantly deferring to a given dependency's actual implementation, this saves you from having to set the config flag every time you write a new test.
angular.module('SomeMocks', [])
.mockService('$http', function($http){
return $http; // returns the actual implemenation of $http
});
quickmock({
providerName: '...',
moduleName: '...',
mockModules: ['SomeMocks']
});
In order to retrieve the list of dependencies for any given provider, quickmock has to instantiate the angular modules provided in the moduleName
and mockModules
properties of the config object. These modules are instantiated the moment quickmock({...})
is called. (Traditionally, this was done in beforeEach
blocks using the module()
method provided in the ngMock
module.) As a side effect of this pre-instantiation, all .config()
and .run()
blocks declared in those modules will be run at that moment. So any code in those blocks will also be executed.
As a rule, quickmock does not inject mocked versions of any of the dependencies for a .config
or .run
block. So these code blocks will receive the actual implemenations of the services/providers they depend on so that the code in these blocks will function as expected. If anyone would like to see this changed, please submit an issue and it can be discussed.
If you wish to test code in a .config()
or a .run()
block, it is recommended that you not use quickmock. Instead, simply call angular's module('myModule')
function, which will instantiate the myModule
module and execute these code blocks.
npm install quickmock
As mentioned above, quickmock works with most of the popular JavaScript unit testing frameworks. It has been tested with Jasmine 1.3+, Mocha 2.0+, Sinon.js, Chai.js. It is expected to work with nearly any others. The exception to this rule is with the spyOnProviderMethods: true
config option. This option only works with Jasmine and with Mocha (when using sinon.js).
An example karma config file can be found here. You will need to include the following files
in your karma config file:
files: [
'vendor/angular.js',
'vendor/angular-mocks.js',
'src/quickmock.js',
'src/quickmock.mockHelper.js', // optional
'<source code>.js',
'<test specs>.js',
'<any file(s) containing mocks>.js'
]
In the <head>
of the SpecRunner.html file, you will need to include references to the following files:
<!-- include vendor files here... -->
<script src="https://github.com/tennisgent/quickmock/raw/master/vendor/angular.js"></script>
<script src="https://github.com/tennisgent/quickmock/raw/master/vendor/angular-mocks.js"></script>
<script src="https://github.com/tennisgent/quickmock/raw/master/vendor/quickmock.js"></script>
<!-- include source files here... -->
<script src="https://github.com/tennisgent/quickmock/raw/master/<source code>.js"></script>
<!-- include spec files here... -->
<script src="https://github.com/tennisgent/quickmock/raw/master/<test specs>.js"></script>
I recently gave a presentation at the AngularJS Utah Meetup where I went over a basic introduction to testing, mocking and how to use quickmock. The slides for that presentation are here. The presentation can be watched on YouTube. I apologize for the poor video quality.
If you have any ideas for how to make quickmock better, please submit them as pull requests.
If you find any issues or bugs, please submit them as issues on this repository.