I need to cover this service 100% in jasmine test but it didn't let me cover the controller.
Here's the service for dialog:
(function () {
'use strict';
angular.module('core.dialog').service('dialog', ToasterService);
ToasterService.$inject = ['$translate', '$mdDialog'];
function ToasterService($translate, $mdDialog) {
this.show = function (key, values) {
$translate(key, values).then(function (message, $event) {
$mdDialog.show({
controller: DialogController,
templateUrl: 'app/common/core/dialog/dialog.html',
targetEvent: $event,
locals: {
message : message
}
});
function DialogController($scope, $mdDialog, message) {
$scope.message = message;
$scope.closeDialog = function() {
$mdDialog.hide();
};
}
});
};
}
})();
but when I run the code coverage report it didn't cover the DialogController function.
Can somebody help me with this? Thanks in advance.
This is an old question, but in case anyone is wondering how this might be solved, here is a way, one of several. I would create the controller with angular, move it out of the service, so you can unit test only the controller. Then spyOn $mdDialog.hide and make sure it was called. Assuming the DialogController(...) is moved out of service so it is visible.
angular.module('example')
.controller('DialogController' DialogController);
Then, when setting up your test store $mdDialog for spying later, you also need $q or whatever promises you want to use if you are doing anything with the promise returned from hide().
var mdDialog, q;
beforeEach(inject(function ($injector) {
mdDialog = $injector.get("$mdDialog");
q = $injector.get("$q");
Then you create your controller and $rootScope.$digest(), which is covered elsewhere in-depth.
And finally in your tests:
spyOn(mdDialog, 'hide').and.callFake(function(){
var deferred = q.defer();
deferred.resolve();
return deferred.promise;
});
// TODO: call your closeDialog() controller function here (may need to call $apply)
expect(mdDialog.hide).toHaveBeenCalled();
Related
I cannot get the test result to pass I'm using a very basic implementation to understand testing deeper.
I have a factory which returns a promise, accessed from my controller. I want to test that the call succeeds and assigns the response to the repos var. Following is the code:
'use strict';
angular.module('app')
.factory('searchServ', function ($timeout, $q, $http) {
return {
fetch: function(user) {
var deferred = $q.defer();
$timeout(function(){
$http({method: 'GET', url: 'https://api.github.com/users/' + user + '/repos'}).then(function(repos) {
deferred.resolve(repos.data);
}, function(reason){
deferred.reject(reason.status);
console.log(reason);
});
}, 30);
return deferred.promise;
}
};
})
.controller('MainCtrl', function ($scope, searchServ) {
$scope.results = function(user) {
$scope.message = '';
searchServ.fetch(user).then(function (repos) {
if(repos.length){
$scope.message = '';
$scope.repos = repos;
}
else{
$scope.message = 'not found'
}
}, function (){
$scope.message = 'not found';
});
};
});
//Test
'use strict';
describe('MainCtrl', function () {
var scope, searchServ, controller, deferred, repos = [{name: 'test'}];
// load the controller's module
beforeEach(module('app'));
beforeEach(inject(function($controller, $rootScope, $q) {
searchServ = {
fetch: function () {
deferred = $q.defer();
return deferred.promise;
}
};
spyOn(searchServ, 'fetch').andCallThrough();
scope = $rootScope.$new();
controller = $controller('MainCtrl', {
$scope: scope,
fetchGithub: fetchGithub
});
}));
it('should test', function () {
expect(scope.test).toEqual('ha');
});
it('should bind to scope', function () {
scope.results();
scope.$digest();
expect(scope.message).toEqual('');
//expect(scope.repos).not.toBe(undefined);
});
});
Running the test gives me the following error :
TypeError: undefined is not a function (evaluating 'spyOn(searchServ, 'fetch').andCallThrough()') in test/spec/controllers/main.js (line 15)
Any idea how I can test this such that it tests the scope binding as well as the async call?
There are a lot of issues with your code.
I've created this Plunkr for the purpose. index.js is the file with your code and test cases. I've edited most of the part according to the conventions and best-practices.
There are a few pointers I wanted to give you:
Since $http returns a promise, you should use that, instead of resolving the promise and creating another promise from your method. Not sure why is timeout used. So I removed $q and $timeout from searchServ's dependencies.
I did the same in the test case by removing the deferred variable that you used.
You should be using angular-mocks.js to mock your services and other dependencies instead of defining a service inside your test case(The way you have did.)
You should create separate describe blocks for testing different parts of your code(a controller in this case).
Hope this helps!
This is my service
angular.module('providers',)
.provider('sample', function(){
this.getName = function(){
return 'name';
};
this.$get = function($http, $log, $q, $localStorage, $sessionStorage) {
this.getTest = function(){
return 'test';
};
};
});
This is my unit test
describe('ProvideTest', function()
{
beforeEach(module("providers"));
beforeEach(function(){
module(function(sampleProvider){
sampleProviderObj=sampleProvider;
});
});
beforeEach(inject());
it('Should call Name', function()
{
expect(sampleProviderObj.getName()).toBe('name');
});
it('Should call test', function()
{
expect(sampleProviderObj.getTest()).toBe('test');
});
});
I am getting an error Type Error: 'undefined' is not a function evaluating sampleProviderObj.getTest()
I need a way to access function inside this.$get . Please help
You should inject your service into the test. Replace this:
beforeEach(function(){
module(function(sampleProvider){
sampleProviderObj=sampleProvider;
});
});
beforeEach(inject());
With this:
beforeEach(inject(function(_sampleProvider_) {
sampleProvider = _sampleProvider_;
}));
Firstly, you need, as had already been said, inject service, that you test. Like following
beforeEach(angular.mock.inject(function ($injector) {
sampleProviderObj = $injector.get('sample');
}));
Second, and more important thing. Sample have no any getTest functions. If you really need to test this function, you should as "Arrange" part of your test execute also $get function of your provider. And then test getTest function of result of previous execution. Like this:
it('Should call test', function()
{
var nestedObj = sampleProviderObj.$get(/*provide correct parameters for this function*/)
expect(nestedObj.getTest()).toBe('test');
});
But it's not good because this test can fail even if nestedObj.getTest work properly (in case when sampleProviderObj.$get works incorrect).
And one more thing, it seems like you need to inject this services $http, $log, $q, $localStorage, $sessionStorage to you provider rather then passing them as parameters.
I'm trying to test function calls from my controller's init function. I route various logic based on stateParams and want to write unit tests against this scenarios but I'm having trouble getting it working.
My init function
var _init = function () {
// Get full companyList
servicecall().then(function (response) {
if ($stateParams.ID) {
$scope.callOne();
}
else if ($stateParams.Index == -1) {
$scope.callTwo();
}
else if ($stateParams.Index == '' || $stateParams.Index == null) {
$scope.callThree();
}
else
$scope.callFour();
},
function (err) {
console.log(err);
});
};
_init();
So simply I want to set $stateParams.Index = -1, and make sure callTwo() gets called.
My beforeEach looks like
beforeEach(function () {
controller = $controller('Controller', {
$scope: $scope,
$stateParams: $stateParams,
$state: $state
});
// And add $scope's spyOns
spyOn($scope, 'callOne').and.callThrough();
spyOn($scope, 'callTwo').and.callThrough();
spyOn($scope, 'callThree').and.callThrough();
spyOn($scope, 'callFour').and.callThrough();
});
At first I tried the below, which of course did not work; it says spy was not called.
it("should call callTwo if stateParams.index is -1", function () {
$stateParams.Index = -1;
expect($scope.callTwo()).toHaveBeenCalled();
});
I figured that all of the init was happening before my spy attached, so then I tried moving that stuff inside of my it call but everything broke down.
This one says callTwo has already been spied upon.
it("should call callTwo if stateParams is -1", function () {
$stateParams.Index = -1;
spyOn($scope, 'callTwo').and.callThrough();
controller = $controller('Controller', {
$scope: $scope,
$stateParams: $stateParams,
$state: $state
});
expect($scope.callTwo).toHaveBeenCalled();
});
But if I move the spy declaration after the controller is initialized it says it's not called again
How can I ensure calls are being made as expected during a controller instantiation?
Not sure if it's the best solution, but currently nothing else comes to mind and this definitely works.
You can have a describe for every such test, since the $stateParams are injected when you create your controller. That's why doing it inside the it is not sufficient since the $scope at that time belongs to the controller already created.
So you need to have:
describe('Controller tests for when Index === -1', function () {
beforeEach(function (...) {
$stateParams.Index = -1;
controller = $controller('Controller', {
$scope: $scope,
$stateParams: $stateParams,
$state: $state
});
}
// it tests for Index === -1 case
});
So in this example all of the it tests you'll have are guaranteed to have $stateParams === 1. Same will go for your other values.
I have a controller that takes a dependency on a service, and as part of it's initialisation calls a function on the service. Here's a contrived example:
describe('tests', function() {
var _scope, service, serviceValue = 'value';
beforeEach(module('app'));
beforeEach(inject(['$rootScope','$controller', function($rootScope, $controller) {
_scope = $rootScope.$new();
service = {
get: function(key) {
return serviceValue;
}
};
$controller('myController', {
'$scope': _scope,
'service': service
});
}]));
describe('initialisation', function() {
describe('key exists', function() {
it('should find the key', function() {
expect(_scope.message).toBe('found the key');
});
});
describe('key does not exist', function() {
beforeEach(function() {
serviceValue = undefined;
});
it('should not find the key', function() {
expect(_scope.message).toBe('did not find the key');
});
});
});
});
angular.module('app').controller('myController', ['$scope','service',
function($scope, service) {
if(service.get('key') === 'value') {
$scope.message = 'found the key';
} else {
$scope.message = 'did not find the key';
}
});
The tests for when the key does not exist fail because the controller initialisation has run in the first beforeEach, before the next beforeEach runs to change the service return value.
I can get around this by recreating the whole controller in the beforeEach of the 'key does not exist' tests, but this seems wrong to me, as it initialises the controller twice for the test. Is there a way to get the controller initialisation to run for every test, but after all other beforeEach functions have run.
Is this the right way to be initialising controllers? Am I missing some feature of jasmine?
Creating the controller for each test is the recommended way, especially when you have initialization logic.
I would however use Jasmine's spyOn to set up what the service returns and tracking calls to it, instead of modifying internal values of a mocked or real service.
Inject the real service and save it in a variable, and define a function that creates the controller:
describe('tests', function() {
var $scope, createController, service;
beforeEach(function() {
module('app');
inject(function($rootScope, $controller, _service_) {
$scope = $rootScope.$new();
service = _service_;
createController = function() {
$controller('myController', {
'$scope': $scope,
'service': service
});
};
});
});
For each test use spyOn to intercept calls to the service and decide what it should return, then create the controller:
describe('initialisation', function() {
it('should find the key', function() {
spyOn(service, 'get').and.returnValue('value');
createController();
expect($scope.message).toBe('found the key');
});
it('should not find the key', function() {
spyOn(service, 'get').and.returnValue(undefined);
createController();
expect($scope.message).toBe('did not find the key');
});
});
Demo: http://plnkr.co/edit/BMniTis1RbOR0h5O4kZi?p=preview
As spyOn sets up tracking you can now for example also make sure the service only gets called once on controller initilization:
spyOn(service, 'get').and.returnValue('value');
expect(service.get.calls.count()).toEqual(0);
createController();
expect(service.get.calls.count()).toEqual(1);
Note: The examples above use Jasmine 2.0. Syntaxes will have to be slightly modified for older versions.
I have a controller with one function exposed in the $scope that calls a service, loginService
$scope.validateCredentials = function (callback){
loginService.validate($scope.username, $scope.password)
.then(function (){
self.credentialsRight();
if (typeof callback !== "undefined") {
callback("credentials right callback");
}
}, function (){
self.credentialsWrong();
if (typeof callback !== "undefined") {
callback("credentials wrong callback");
}
})
}
I have managed to test if the beforementioned method works right testing it with jasmine like this
it("validateCredentials - passing in the right credentials", function() {
spyOn(loginService, 'validate').andCallFake(function() {
return successfulDeferred.promise;
});
$scope.validateCredentials(function() {
expect($scope.error).toBe(false);
});
$scope.$apply();
});
The reason why I call andCallFake using an spy is because I want to fake a promise being returned. Please note I have tried to do this using $httpBackend.onPOST unsusccefully.
However, I feel that the usage of a callback in my controller only for testing purposes and deal with an async response is weird. Do you guys know a better way to implement it? I have seen waitsFor and runs but doesn't seem to work for me.
Here is a similar test that I have in my code base. You will need to adapt for your solution. But basically you need to call deferred.reject(someData) after you call the service method. Then you may need to call a digest on the root scope. Then test your expectation. Replace personApi with your loginService.
//setup the controller
var $scope, ctrl, deferred, personApi, rootScope, log,basePerson;
beforeEach(inject(function ($rootScope, $q, $controller, _personApi_) {
rootScope = $rootScope;
$scope = $rootScope.$new();
deferred = $q.defer();
personApi = _personApi_;
ctrl = $controller('personCtrl', {
$scope: $scope,
personApi: personApi
});
}));
it('should map returned errors to the original object', function () {
spyOn(personApi, 'save').and.callFake(function () {
return deferred.promise;
});
$scope.person = basePerson;
deferred.reject(stronglyNamedErrors);
$scope.savePerson();
$scope.$root.$digest();
expect($scope.person.firstNameError).toBe(stronglyNamedErrors.firstNameError);
});