I have been writing some Jasmine unit tests in Angular. In the first example I'm testing a controller.
myApp.controller('MyCtrl', function($scope, Config){
...
});
I have a configuration service (Config) that keeps configuration from the database and is injected into my controller. As this is a unit test, I want to mock out that configuration service altogether, rather than allowing execution to pass through it and using $httpBackend. Examples I found taught me about a $controller function I can use like this, in order to get an instance of my controller with my mocks injected in place of the usual collaborator:
beforeEach(inject(function($controller, $rootScope){
var scope = $rootScope.$new();
var configMock = {
theOnlyPropertyMyControllerNeeds: 'value'
};
ctrl = $controller('MyCtrl', {
$scope:scope,
Config: configMock
});
}));
But I also have other services that use the Config service. To help unit test them, I assumed there would be a similar $service function I could use to instantiate a service with whatever mocks I want to provide. There isn't. I tried $injector.get, but it doesn't seem to let me pass in my mocks. After searching for a while, the best I could come up with in order to instantiate a service in isolation (avoid instantiating its collaborators) is this:
beforeEach(function() {
mockConfig = {
thePropertyMyServiceUses: 'value'
};
module(function($provide) {
$provide.value('Config', mockConfig);
});
inject(function($injector) {
myService = $injector.get('MyService');
});
});
Is this the right way? It seems to be overriding the entire application's definition of the Config service, which seems maybe like overkill.
Is it the only way? Why is there no $service helper method?
For unit testing, it is common that you override a service for the sake of testing. However, you can use $provide to override an existing service instead of using inject, as long as you load the application before hand.
Assuming that you created Config using something like:
angular.moduel('...', [...]).factory('Config', function (...) {...});
If so, try this:
...
beforeEach(module("<Name of you App>"));
beforeEach(
module(function ($provide) {
$provide.factory('Config', function (...) {...});
});
);
...
After that, when you initialise your controller, it will get the mocked Config.
Related
I'm trying to write unit tests for an angular service with jasmine/karma. I have a similar service test, which works just fine. But this one has some additional dependencies, is in a different module and just doesn't find the service with the inject.
The service looks something like this. bService is in the same module, but commonFactory and commonService are in another module, say commonModule.
(function () {
'use strict';
angular
.module('myService')
.service('aService', aService);
aService.$inject = [
'commonFactory',
'commonService'
'bService'
];
function aService (
commonFactory,
commonService,
bService
) {
};
return {
codeIWantToTest: CodeIWantToTest;
}
function CodeIWantToTest () {
console.log('surprise!');
}
})();
My jasmine test looks like:
describe('myService.aService', function () {
'use strict';
var aService;
// I tried adding beforeEach(module('commonModule')); too, but that didn't do anything
beforeEach(module('myService'));
beforeEach(function() {
inject(function(_aService_) {
console.log('getting aService');
aService = _aService_;
});
});
it('tests my service is defined', function() {
expect(myService).toBeDefined();
});
});
This test fails. myService isn't defined and the console.log in the inject function doesn't ever fire. My karma.conf.js basically lists the dependencies in the order that they're injected into the service, then adds the service then the test.
What would cause the inject to not grab the service? What am I missing? I mentioned I have a similar test for commonService and it works just fine. So I'm baffled.
Another dev on my team found the solution and I wanted to post it as an answer for the future people. I had a feeling it was a dependency problem, and it was. While we were loading all of the JS stuff correctly, the template that the component uses was loading another js dependency. So to fix this for jasmine, we had two different solutions:
at the top of the component test file, we could add:
beforeEach(function () {
module(function ($provide) {
$provide.constant('myMissingDependency', {
// do things to load the dependency here
});
});
});
In our case it was a translation library
The other solution was to add a 'shim' file into the unit test directory and load it with karma.config.js ahead of the tests. That looked like:
(function() {
angular
.module('MyService')
.constant('myMissingDependency', Object.freeze({
// things to load the dependency
}));
})();
I wasn't able to switch to Chrome because we're using Docker and I couldn't get the tests to run locally to run Chrome. So adding a second set of eyes to this was what I needed.
I have a service depending on $controller service. Inside of this service, $controller service takes a controller name and locals to instantiate this controller.
When I unit test this service, I would like to pass a dummy controller name, so I can test this service properly.
From reading angular.js source code, I know $controller service looks for registered controllers by controller name. Controllers are registered through register method of $controllerProvider. How can I access this method in unit test. I'm using Jasmine here for unit testing.
Any advices are appreciated.
If you are trying to test Angular's $controller service, and assuming you are using ngMocks, you can use $controller normally. In Angular's documentation, there's an example on how to use it.
Anyway, here's the sample from the docs:
describe('myDirectiveController', function() {
it('test case', inject(function($controller) {
// Your code goes here
});
});
beforeEach(function () {
module('myApp', function ($controllerProvider) {
$controllerProvider.register(dummyController, function () { });
});
});
I ended up with this. when my service call $controller(dummyController, controllerlocals), it was able to find this dummyController registered in its local variable controllers.
Hope this would help people have the same scenario
In Angular unit testing, it takes a lot of unnecessary code to mock/stub all depencencies manually, especially when all I need are some generic mocks. Also, when dependency list changes for service/controller, tests break because of missing dependencies.
In C#, there is a way to reconfigure DI container to automatically mock all dependencies, when Resolve() is called. I want something like that in Angular.
I want to go from this:
beforeEach(inject(function ($controller, $rootScope, agsRest) {
scope = $rootScope.$new();
sut = $controller('SearchController', {
$scope: scope
, map: {}
, agsRest: agsRest
, config: {}
, core: {}
, myPopup: {}
, popupFormatter: {}
});
}));
To something like this:
beforeEach(inject(function ($controller, $rootScope, agsRest) {
scope = $rootScope.$new();
sut = autoMock("SearchController"); // instance of SearchController is returned, which has all dependencies mocked with sinon/jasmine/whatever
}));
Is there some kind of library / code to do this?
I've needed something similar and came up with this small utility library: tentacle.js. I'm up to suggestions and to pull requests for bettering the API.
You should look for ng-improved-testing. It's create auto-mock for all injected services (you need only add "Mock" to services name when you inject them).
Link to GitHub:
https://github.com/evangalen/ng-improved-testing
When making use of a service in a controller test do you need to initialize the service in the same way you would the controller? By this I mean do you need to pass it its own dependencies?
For example I can initialize my controller like so:
// Instantiate the controller
searchController = $controller( 'VisibilitySearchController',{
$scope: scope,
dataService: dataService
});
}));
so do I need to initialize the service according to the components it needs like $http, $resource etc as well as make spyOn calls on its functions? Or is this/should this be sufficient? (Note - my tests fail when I do the following )
// Instantiate the dataService
dataService = $injector.get( 'dataService' );
it throws this error:
* Error: [$injector:unpr] Unknown provider: $resourceProvider <- $resource <- dataService
The relevant part of the service:
myAppServices.factory('dataService', ['$http', '$resource', 'authService', 'messageService', function ($http, $resource, authService, messageService) {
}
Side note
Note - we are using Maven as our build tool and only make use of Jasmine at this point - trying to bring Karma in as our test-runner as a Maven plugin.
You must provide all the dependencies but you can mock them. This can be done by jasmine like this for example:
var mockedDataService = jasmine.createSpyObj('dataService', ['getData', 'getOtherData']);
And then you inject this mocked service to $provider:
beforeEach(function () {
module(function ($provide) {
$provide.value('dataService', mockedDataService );
});
}
Instance of this mocked service can be retrieved like this then:
inject(function (dataService) {
var dataServiceInstance = dataService;
});
This will provider mocked dataService anytime it is needed. However if you need fully functional dataService you must instantiate it but always you can mock any of its dependecies.
While you can inject dependencies into the controller manually you don't need to do it as long as you have loaded the module the service belongs to.
In your case it looks like you have not loaded the ngResource module.
If you add beforeEach(module('ngResource')) to your test (and make sure the actual script file it lives in is included in Jasmine's fileset) you should not need to inject it manually.
Note that you do not need to load angular core services like $http, but since $resource is not part of core it needs to be loaded like this.
Injecting dependencies manually is mostly useful if you want to provide a mock implementation.
Is it possible to do DI in a provider method?
In this example
angular.module('greet',[])
.provider('greeter',function() {
this.$get=function() {
};
})
.service('greeterService',function($http){
console.log($http);
})
;
Injecting $http into service appears to be the correct implementation, but it doesn't work in a provider method and it throws an error:
Unknown provider: $http
Does the provider method work with DI to inject services?
You can certainly inject $http to provider. Just make sure it appears in $get, not the function constructor. As follows:
angular.module('greet',[]).provider('greeter',function() {
this.$get = function($http) {
};
});
You can inject constants and other providers into a provider. Not services or factories - with one exception. It seems that you can inject the $injector service into a provider - at least, you can in AngularJS 1.3.16.
.provider('foo', ['$injector', function ($injector) {
var messagePrefix = $injector.get('msgPrefix');
this.message = '';
this.$get = function() {
var that = this;
return function() {
return messagePrefix + that.message;
}
};
}])
You can use the injector outside the $get method, but you still can't get services from it at configure time.
See here for a demo.
Following up on IgrCndd's answer, here's a pattern that might avoid potential nastiness:
angular.module('greet',[]).provider('greeter', function() {
var $http;
function logIt() {
console.log($http);
}
this.$get = ['$http', function(_$http_) {
$http = _$http_;
return {
logIt: logIt
};
}];
});
Note how similar this is to the equivalent service, making conversion between the two less troublesome:
angular.module('greet',[]).factory('greeter', ['$http', function($http) {
function logIt() {
console.log($http);
}
return {
logIt: logIt
};
});
You actually have to inject the dependency on $get and then store it to use on what you retrieve from $get. Not beautiful at all...
No, you can not inject a service into the provider itself.
Injecting a service into a provider's $get method is the same as injecting a service into a factory, but you can not inject it into the provider function directly.
The difference between $get and the provider itself is that the provider runs during the module loading phase whereas the $get is run when instantiating the service you are providing.
This implies that you can not use any service at all during the module loading/configuration phase of your modules. That is all the stuff you run inside your config blocks, such as when defining your app routes or states, can not make use of any service.
The only other thing you can inject into config blocks besides providers are constants.
You could do something like IgrCndd suggested. But if you needed to consume the provider in a config block, which is the provider's purpose after all, you will not have your values injected until much after. So it's not going to work unless you do some nasty hack using promises.
Further reading on injectables