I've looked at the documentation for angular.mock.module and a couple of examples of others using it but I seem to be running into an issue in my use-case that I don't understand.
I'm running Jasmine (2.4.1) tests with angular (1.4.9) and I have my angular app separated into multiple modules. When I attempt to mock out certain parts of my app for unit testing I want to mock out entire modules (or providers) so that I only expose the pieces I use.
Here is a very simple app that has a main module plunker which depends on plunker.service. plunker.service depends on plunker.constant.
var app = angular.module('plunker', ['plunker.service']);
app.controller('MainCtrl', function($scope, valueService, appService) {
$scope.init = function() {
$scope.appValue = valueService.getValue();
$scope.appIsRunning = appService.getStatus();
};
});
angular.module('plunker.service', ['plunker.constant'])
.service('appService', function(appSettings) {
var vm = this;
vm.getStatus = function () {
if (appSettings.isRunning) {
return true;
} else {
return false;
}
};
})
.service('valueService', function(valueSettings) {
var vm = this;
vm.getValue = function () {
return valueSettings.value;
}
});
angular.module('plunker.constant', [])
.constant('appSettings', { isRunning: true })
.constant('valueSettings', { value: 10 });
In my Jasmine tests I have a beforeEach() that registers my modules using module (aka angular.mock.module).
I have seen 3 ways of using module
string
function with $provide
object
You can see below that I use the module('plunker') (string) to register my main module and I have 3 ways of mocking out my appSettings constant (A, B, C). You will notice that the function with $provide.constant works fine but function with $provide.value does not and object does not.
beforeEach(function() {
module('plunker');
function useFunction(typeofProvider) {
module(function($provide) {
$provide[typeofProvider]('appSettings', { isRunning: false });
});
}
function useObject() {
module({
appSettings: { isRunning: false }
});
}
// A. THIS WORKS! //
useFunction('constant');
// B. THIS DOES NOT //
// useFunction('value');
// C. THIS ALSO DOES NOT!! //
// useObject();
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
});
});
I have also seen people use the following syntax...
beforeEach(function() {
var mockService = function () {
var mockValue = 10;
this.value = mockValue;
};
// D.
module('a.module.name', function newProviders($provide){
$provide.service('realService', mockService);
});
});
My questions
In my test code, why does A. work but B. and C. do not?
Is D. equivalent to calling module('a.module.name'); followed by module(function newProviders($provide) { ... });? Does placing both in the same module() call have any special effects on how things are registered or is it just a shorthand? (based on the documentation it should be a shorthand)
Related to Jasmine, specifically, do all beforeEach() calls run in the same top-to-bottom order with every execution?
Here is my plunker for the above app and jasmine code
Thanks
This happens because of how Angular injector works. In fact, there are two different injectors in Angular. The one (available as $injector in config blocks) deals with service providers. Another one (available as $injector anywhere else) deals with service instances. Providers and instances are cached and stored internally.
$provide.constant('service') creates both provider and instance of name 'service' at call time.
All other types of services are lazily instantiated. They create 'serviceProvider' provider at call time, but 'service' instance is created on the first injection.
Since Angular service instance is a singleton, it refers to instance cache before the instantiation. If the instance is in the cache, it is reused and not instantiated. constant service instance is eagerly instantiated, so only another constant can override the instance.
Object properties in angular.mock.module are shortcuts for $provide.value, and useObject() equals to useFunction('value') in this example.
As long as module order stays the same,
module('a.module.name', function ($provide) { ... });
is indeed a shortcut for
module('a.module.name');
module(function ($provide) { ... });
Due to the fact that appSettings object isn't used in config blocks (the primary use of constant service), it is more convenient to make it value.
Related
I'm currently getting started with angular unit testing. As the first controller I wanted to tes looked like this, I got confused.
angular.module('sgmPaperApp')
.controller('AccountCtrl', function ($mdToast, user, $firebaseArray, Ref) {
var vm = this;
vm.data = user;
vm.save = saveUser;
vm.comments = $firebaseArray(Ref.child('comments').orderByChild('person').equalTo(user.$id));
function saveUser() {
vm.data.$save().then(function () {
$mdToast.showSimple('Data saved');
});
}
});
Should I really mock all external services I use? After all that controller isn't very much more then external services and mocking the firebaseArray could be difficult.
Thanks for your advice and helping me get started with testing
You don't need to worry about what the external dependencies do, just mock their APIs.
These are the only mocks I can see. I'm going to assume you're using Jasmine
var Ref, $firebaseArray, $mdToast, user, vm;
beforeEach(function() {
Ref = jasmine.createSpyObj('Ref', ['child', 'orderByChild', 'equalTo']);
Ref.child.and.returnValue(Ref);
Ref.orderByChild.and.returnValue(Ref);
Ref.equalTo.and.returnValue(Ref);
$firebaseArray = jasmine.createSpy('$firebaseArray').and.returnValue('comments');
$mdToast = jasmine.createSpyObj('$mdToast', ['showSimple']);
user = jasmine.createSpyObj('user', ['$save']);
user.$id = 'id';
module('sgmPaperApp'); // you should consider separate modules per "thing"
inject(function($controller) {
vm = $controller('AccountCtrl', {
$mdToast: $mdToast,
user: user,
$firebaseArray: $firebaseArray,
Ref: Ref
});
});
});
Then you can easily create your tests
it('assigns a bunch of stuff on creation', function() {
expect(vm.data).toBe(user);
expect(vm.comments).toEqual('comments'); // that's what the mock returns
expect(Ref.child).toHaveBeenCalledWith('comments');
expect(Ref.orderByChild).toHaveBeenCalledWith('person');
expect(Ref.equalTo).toHaveBeenCalledWith(user.$id);
expect($firebaseArray).toHaveBeenCalledWith(Ref);
});
You can even test promise based methods like saveUser
it('saves the user and makes some toast', inject(function($q, $rootScope) {
user.$save.and.returnValue($q.when()); // an empty, resolved promise
vm.saveUser();
expect(user.$save).toHaveBeenCalled();
expect($mdToast.showSimple).not.toHaveBeenCalled(); // because the promise hasn't resolved yet
$rootScope.$apply(); // resolves promises
expect($mdToast.showSimple).toHaveBeenCalledWith('Data saved');
}));
So to answer the question we need to consider what we're actually trying to do. If we are trying to unit test, then yes, we need to mock all dependencies.
Mocking your dependencies won't be hard though. You only need to mock what you're using.
For example, $firebaseArray starts off as a function that receives a paramter, we know that much:
var mockFirebaseArray = function(ref) {
};
Next, before we can finish it, we need to mock the Ref:
var mockRef = {
child: function(path) {
this.orderByChild = function(path) {
this.equalTo = function(val) {
};
return this;
};
return this;
}
};
With these things in place we can decide how the test will "pass". We could just use spies. Or, we could set local variables that we can assert later on our way through.
Spies are my preferred method because you can even verify they were called with specific values:
expect(mockFirebaseArray).toHaveBeenCalled();
expect(mockRef.child).toHaveBeenCalledWith('comments');
Now, if you're wanting to write an integration test that's different. In that case I'd still use spies, but you'd actually be executing those dependencies. Generally speaking there is no need to test your dependencies because they should be tested in isolation as well. Furthermore, there is less need to test other people's API's if they are from trustworthy sources.
I wish to reuse my mocks instead of having to set them up in every unit test that has them as dependency. But I'm having a hard time figuring out how to inject them properly.
Here's my attempt at unit test setup, which of course fails because ConfigServiceMockProvider doesn't exist.
describe('LoginService tests', function () {
var LoginService;
beforeEach(module('mocks'));
beforeEach(module('services.loginService', function ($provide, _ConfigServiceMock_) {
$provide.value("ConfigService", _ConfigServiceMock_);
/* instead of having to type e.g. everywhere ConfigService is used
* $provide.value("ConfigService", { 'foobar': function(){} });
*/
});
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
});
}
ConfigServiceMock
angular.module('mocks').service('ConfigServiceMock', function() {
this.init = function(){};
this.getValue = function(){};
}
I realize I probably could have ConfigServiceMock.js make a global window object, and thereby not needing to load it like this. But I feel there should be a better way.
Try something like this:
describe('Using externally defined mock', function() {
var ConfigServiceMock;
beforeEach(module('mocks'));
beforeEach(module('services.configService', function($provide) {
$provide.factory('ConfigService', function() {return ConfigServiceMock;});
}));
beforeEach(module('services.loginService'));
beforeEach(inject(function (_ConfigServiceMock_) {
ConfigServiceMock = _ConfigServiceMock_;
}));
// Do not combine this call with the one above
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
}));
it('should have been given the mock', function() {
expect(ConfigServiceMock).toBeDefined('The mock should have been defined');
expect(LoginService.injectedService).toBeDefined('Something should have been injected');
expect(LoginService.injectedService).toBe(ConfigServiceMock, 'The thing injected should be the mock');
});
});
According to this answer, you have to put all of your calls to module before all of your calls to inject.
This introduces a bit of a catch-22 because you have to have the reference to your ConfigServiceMock (via inject) into the spec before you can set it on the LoginService (done in the module call)
The work-around is to set an angular factory function as the ConfigService dependency. This will cause angular to lazy load the service, and by that time you will have received your reference to the ConfigServiceMock.
I am totally new to testing in AngularJS. I have setup karma, and am now attempting to test a certain function in a factory I have written.
Here is a snippet of my factory:
app.factory('helpersFactory', ['constants', function (constants) {
return {
someFunction: function() {
},
is24x24Icon: function (iconNum) {
return ((iconNum >= 10090 && iconNum <= 10125) ;
}
};
}]);
I then have this test:
describe('Factory: helpersFactory', function () {
beforeEach(module('ppMobi'));
var fct;
beforeEach(inject(function ($factory) {
fct = $factory('helpersFactory');
}));
it('should detect iconNum 10090 is a 24 x 24 icon', function () {
var iconNum = 10090;
var is24x24Icon = fct.is24x24Icon(iconNum);
expect(is24x24Icon).toBeTruthy();
});
});
I get an error from Karma telling me it cannot read 'is24x24icon' of undefined. Therefore I can only assume my factory has not been created properly during the test. I do have a dependency on constants in the factory used by other functions. This is just an angular.constant() I have setup on my main application module.
I have found some other posts, but am unsure how to proceed, do I need to inject my constants dependency into my test?
Kind of new myself but I think you need to use the underscore name underscore trick to inject your factory:
var fct;
beforeEach(inject(function (_helpersFactory_) {
fct = _helpersFactory_;
}));
This blog uses mocha but I found it useful and the Karma stuff should be the same: https://www.airpair.com/angularjs/posts/testing-angular-with-karma
And yes you will need to inject the constants as well (the link shows how) but your posted code does not seem to use constants so you won't need it for this particular test.
I have a project using AngularAMD/RequireJS/Karma/Jasmine, that I have the basic configuration all working, most unit tests run and pass successfully.
I cannot get a mocked service injected correctly using either angular.mock.module or angularAMD.value().
I have:
// service definition in services/MyService.js
define(['app'],
function(app) {
app.factory('myService', [ '$document', function($document) {
function add(html) {
$document.find('body').append(html);
}
return { add: add };
}]);
}
);
// test
define(['angularAMD', 'angular-mocks', 'app', 'services/MyService'],
function(aamd, mocks, app) {
describe('MyService', function() {
var myBodyMock = {
append: function() {}
};
var myDocumentMock = {
find: function(sel) {
// this never gets called
console.log('selector: ' + sel);
return myBodyMock;
}
};
var svc;
beforeEach(function() {
// try standard way to mock a service through ng-mock
mocks.module(function($provide) {
$provide.value('$document', myDocumentMock);
});
// hedge my bets - try overriding in aamd as well as ng-mock
aamd.value('$document', myDocumentMock);
});
beforeEach(function() {
aamd.inject(['myService',
function(myService) {
svc = myService;
}]);
});
it('should work', function() {
// use svc expecting it to have injected mock of $document.
spyOn(myDocumentMock, 'find').andCallThrough();
spyOn(myBodyMock, 'append');
svc.add('<p></p>');
expect(myDocumentMock.find).toHaveBeenCalledWith('body');
expect(myBockMock.append).toHaveBeenCalledWith('<p></p>');
});
});
}
);
Does anyone know where I'm going wrong ? Any help would be much appreciated.
Angular isn't asynchronous, I think is not a good ideia use both. If you're trying to reach to a good modularization method, okay, but use the RequireJS optimizer to build everything before you put this on your browser, and about the tests, I think you can just use RequireJS optimizer to build your modules before, it will let you free from "CommonJS environment even in tests".
Looks like it'll be an issue with variable scopes, karma is very finicky about that. I think you should initialize your mock objects globally, then set them in the beforeEach.
The top line of my test files always looks something like:
var bodyMock, svcMock, foo, bar
Then in the beforeEach'es I set the values
Edit: Since bodyMock is only a scope variable, at the point where the tests are actually running and the browser is looking for an object 'bodyMock', it can't find anything.
Very new to Angular testing... using 1.3.0.rc0. To get started I'm trying to do something simple: get the value of a constant I set. Within a config.js, I have the following:
(function () {
'use strict';
var app = angular.module('app');
// create app configuration
var appConfig = {
version = '0.0.1.0',
debugMode = true
};
app.constant('config', appConfig);
app.config([function ($logProvider, config) {
// set the debugging setting of the app > same setting for the app
if ($logProvider.debugEnabled) {
$logProvider.debugEnabled(config.debugMode);
}
}]);
})();
I'm tried numerous things to write my tests (using jasmine & karma), but I keep getting an error that:
Error: [$injector:modulerr] Failed to instantiate module app due to:
TypeError: 'undefined' is not an object (evaluating '$logProvider.debugEnabled').
I get that this was a bug a while ago in the angular-mocks.js file but has since been resolved. Regardless, no matter the test I write, it doesn't work. Here's what i'm working with now, knowing that there are issues with it.
'use strict';
describe('config.js', function () {
var logProvider;
beforeEach(module(inject(function ($log) {
logProvider = $log;
})));
beforeEach(module('app', logProvider));
it('should set the config constant to the app global configuration settings', function () {
var $injector = angular.injector(['ng', 'app']);
var settings = $injector.get('config');
//var settings = inject(config);
expect(settings.debugMode).toBe(true);
});
});
Am I doing this right? If so, is there no way to get around the test issue with $logProvider?
There is much to learn about how modules work in Angular, especially under testing with ngMocks. I'll try to be brief.
One always begins by calling module (from ngMocks) one (or more times) to build up the module "cookbook" for a test run.
In any of these module calls you have an opportunity to access and stash away a previously defined provider.
The first time you call inject (from ngMocks) in a given test path, the module "cookbook" is "baked" for that path and the injector is populated based on recipes in that "cookbook".
Subsequent calls to module are irrelevant. Your expression beforeEach(module('app', logProvider)); executes too late (even if it did what you wanted, which it would not).
In fact, I'm surprised that you didn't get the error: "Error: Injector already created, can not register a module!".
inject always returns the thing created by the provider, never the provider itself. Your first beforeEach ...
beforeEach(module(inject(function ($log) {
logProvider = $log;
})));
... actually sets logProvider to the $log service, not the $logProvider.
Does this help?
Here is a sample from my forthcoming course on Ng testing that shows how to access a provider (in this case, the $logProvider). It was inspired by your question.
First, the config2 constant (I already had a value called config:
// my sample application module definition is called 'basics'
var basics = angular.module('basics', []);
/* define 'config2' constant - which is available in Ng's config phase */
basics.constant('config2', {
debugMode: true
});
// use constant in config phase
basics.config(function ($logProvider, config2) {
$logProvider.debugEnabled(config2.debugMode);
})
Now the spec (using Mocha and Chai):
describe('Basics - constant:', function() {
'use strict';
beforeEach(module('basics'));
// other stuff
describe("the $logProvider", function(){
var configConstant;
var $log;
var $logProvider;
beforeEach(module(
// Could combine with module('basics') definition in outer describe
// but only need it here in this describe
// This module definition function has access to any previously defined provider
// which in this case is any provider defined in ng, ngMocks, or basics
function( _$logProvider_) {
$logProvider = _$logProvider_;
}
));
// inject triggers injector creation; module definition now "baked"
beforeEach(inject(function(config2, _$log_){
configConstant = config2;
$log = _$log_;
}));
it("is accessible via the module function", function(){
expect($logProvider).to.exist;
});
it("is not the same as the log service", function(){
expect($logProvider).not.to.equal($log);
});
it("has same debugEnabled value as config2.debugMode", function(){
expect($logProvider.debugEnabled()).to.equal(configConstant.debugMode);
});
});
});