I am writing a unit tests for below controller. I have two functions loadCountries and loadTimezones that I want to be called on page load. I want to test that countries are loaded on page load. In that particular test, I don't care whether timezones are loaded or not. So I mocked Timezones services. But looks like I've to return a value for timezones mock. I don't want to handle it explicitly. I was expecting when I do createSpyObj, any function calls that are not explicitly handled on the spy/mock will be dropped or will have no effect. If I don't chain returnValue, mock is calling real function. How do I fix this ?
'use strict';
angular.module('nileLeApp')
.controller('RegisterController', function ($scope, $translate, $timeout, vcRecaptchaService, Auth, Country, Timezone, RecaptchaService) {
$scope.success = null;
$scope.error = null;
$scope.doNotMatch = null;
$scope.errorUserExists = null;
$scope.registerAccount = {};
$timeout(function () {
angular.element('[ng-model="registerAccount.email"]').focus();
});
$scope.loadCountries = function () {
Country.getCountries()
.then(function (result) {
$scope.countries = result.data;
});
};
$scope.loadTimezones = function () {
Timezone.getTimezones()
.then(function (result) {
$scope.timezones = result.data;
});
};
$scope.loadCountries();
$scope.loadTimezones();
});
Below is the test I'm trying.
'use strict';
describe('Register Controllers Tests', function () {
describe('RegisterController', function () {
// actual implementations
var $scope;
var $q;
// mocks
var MockTimeout;
var MockTranslate;
var MockAuth;
var MockCountry;
var MockTimezone;
// local utility function
var createController;
beforeEach(inject(function ($injector) {
$q = $injector.get('$q');
$scope = $injector.get('$rootScope').$new();
MockTimeout = jasmine.createSpy('MockTimeout');
MockAuth = jasmine.createSpyObj('MockAuth', ['createAccount']);
MockCountry = jasmine.createSpyObj('MockCountry', ['getCountries']);
MockTimezone = jasmine.createSpyObj('MockTimezone', ['getTimezones']);
MockTranslate = jasmine.createSpyObj('MockTranslate', ['use']);
var locals = {
'$scope': $scope,
'$translate': MockTranslate,
'$timeout': MockTimeout,
'Auth': MockAuth,
'Country': MockCountry,
'Timezone': MockTimezone
};
createController = function () {
$injector.get('$controller')('RegisterController', locals);
};
}));
it('should load countries on page load', function () {
var mockCountryResponse = {data: [{
'countryId': 1,
'alpha2Code': "AF",
'countryName': "Afghanistan"
}]};
MockCountry.getCountries.and.returnValue($q.resolve(mockCountryResponse.data));
// Want to avoid explicitly specifying below line
MockTimezone.getTimezones.and.returnValue($q.resolve({}));
// given
createController();
$scope.$apply($scope.loadCountries);
expect($scope.countries).toEqual(mockCountryResponse);
});
});
It is not possible to get rid of and.returnValue here, because the controller chains a promise and expects the methods on the object that Timezone.getTimezones stub would return (and it returns none).
jasmine.createSpyObj only handles the calls to Timezone methods and not their return values, that's why and.returnValue is there.
It is totally fine to do
MockTimezone.getTimezones.and.returnValue($q.resolve({}));
in promise-based specs.
Related
I am writing unit tests for my existing AngularJS app. There are just four methods in this service. I was able to getFollowUpList to work, but refresh() is not working and it is a very simple method.
The refresh method should simply set deferredGetFollowUpList = null and return true in my test.
There error I'm getting is: TypeError: Cannot read property 'then' of undefined, so my refresh method is undefined. Why is this the case? Thanks
Service
(function () {
"use strict";
angular
.module("all.patient.details")
.factory("followUpListService", ["$rootScope", "$http", "userContext", "$q", "$uibModal", "htmlBaseUrl", FollowUpListService]);
function FollowUpListService($rootScope, $http, userContext, $q, $uibModal, htmlBaseUrl) {
var deferredGetFollowUpList = null;
return {
getFollowUpList: getFollowUpList,
displayModal: displayModal,
refresh: refresh,
save: save
}
function refresh() {
deferredGetFollowUpList = null;
}
}
})();
Unit Test
describe("followUpListService", function () {
beforeEach(module("all.patient.details"));
var followUpListService = {};
var $httpBackend;
var htmlBaseUrlMock;
var returnedFollowUpListData;
var deferredGetFollowUpList;
var $rootScope;
var $q;
var $uibModal;
beforeEach(function () {
htmlBaseUrlMock = { format: function () { } };
module(function ($provide) {
$provide.value("htmlBaseUrl", htmlBaseUrlMock);
});
inject(function (_$rootScope_, _$httpBackend_, _$q_, _$uibModal_, _followUpListService_) {
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
$q = _$q_;
$uibModal = _$uibModal_;
followUpListService = _followUpListService_;
});
});
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it("calls refresh()", function () {
followUpListService.refresh()
.then(function (data) {
deferredGetFollowUpList = data;
});
expect(deferredGetFollowUpList).toBe(null);
});
As deferredGetFollowUpList is service level variable, Can you write your test as -
followUpListService.deferredGetFollowUpList = data; //Any Mock Data
followUpListService.refresh();
expect(followUpListService.deferredGetFollowUpList).toBe(null);
I'm trying to write a test for an Angular controller that mostly creates a datatable of values from the server. I've tried mocking DTOptionsBuilder and DTColumnBuilder but this doesn't seem to work. I get the error:
'undefined' is not an object (evaluating 'DTOptionsBuilder.fromFnPromise(function(){
return MarketsFactory.getAll();
})
.withDataProp')
Here is the Controller code:
.controller('MarketsCtrl', function($scope, $compile, $state, MarketsFactory,
DTOptionsBuilder, DTColumnBuilder) {
$scope.edit = function(data){
$state.go('admin.market', {id:data});
};
//DATATABLES CONFIGURATIONS
$scope.dtInstance = {};
$scope.dtOptions = DTOptionsBuilder.fromFnPromise(function(){
return MarketsFactory.getAll();
})
.withDataProp('data.data')
.withOption('createdRow', function(row, data, dataIndex) {
$compile(angular.element(row).contents())($scope);
})
.withTableTools('http://cdn.datatables.net/tabletools/2.2.2/swf/copy_csv_xls_pdf.swf')
.withTableToolsButtons([
'copy',
'print', {
'sExtends': 'collection',
'sButtonText': 'Save',
'aButtons': ['csv', 'xls', 'pdf']
}
])
.withBootstrap()
.withBootstrapOptions({
TableTools: {
classes: {
container: 'btn-group right',
buttons: {
normal: 'btn btn-outline btn-default btn-sm'
}
}
}
});
$scope.dtColumns = [
DTColumnBuilder.newColumn('shortName').withTitle('Short Name').withClass('dt-left'),
DTColumnBuilder.newColumn('name').withTitle('Name').withClass('dt-left'),
DTColumnBuilder.newColumn('timezone').withTitle('Time Zone').withClass('dt-left'),
DTColumnBuilder.newColumn('id').renderWith(function(data, type, full) {
return '<a ng-click="edit(\'' + data + '\')">Edit</a>';
})];
})
And the test file:
describe('Controller: MarketsCtrl', function () {
var scope, $state, DTOptionsBuilder, DTColumnBuilder;
beforeEach(function(){
var mockState = {};
var mockDTOptionsBuilder = {};
var mockDTColumnBuilder = {};
module('app', function($provide) {
$provide.value('$state', mockState);
$provide.value('DTOptionsBuilder', mockDTOptionsBuilder);
$provide.value('DTColumnBuilder', mockDTColumnBuilder);
});
inject(function() {
mockState.go = function(target) {
return target;
};
mockDTOptionsBuilder.fromFnPromise = jasmine.createSpy('DTOptionsBuilder.fromFnPromise');
mockDTOptionsBuilder.withDataProp = jasmine.createSpy('DTOptionsBuilder.withDataProp');
mockDTColumnBuilder.newColumn = jasmine.createSpy('DTColumnBuilder.newColumn');
});
});
beforeEach(inject(function ($controller, $rootScope, _$state_, _DTColumnBuilder_, _DTOptionsBuilder_) {
scope = $rootScope.$new();
$state = _$state_;
DTOptionsBuilder = _DTOptionsBuilder_;
DTColumnBuilder = _DTColumnBuilder_;
$controller('MarketsCtrl', {
$scope: scope,
$state: $state,
DTOptionsBuilder: DTOptionsBuilder,
DTColumnBuilder: DTColumnBuilder
});
scope.$digest();
}));
it('should provide an edit function', function () {
expect(typeof scope.edit).toBe('function');
});
});
I thought that creating a mock and putting a Spy on it would prevent it from calling the chained functions, but I guess not.
I'm quite new to testing, especially with Angular, so any help in general would be greatly appreciated!
I'm approaching this from a sinonjs/mocha background
I'm having some trouble decrypting your beforeEach block - but it can be simplified to some extent:
var $scope, $state, DTColumnBuilder, DTOptionsBuilder, createController;
beforeEach(function () {
DTColumnBuilder = {};
DTOptionsBuilder = {};
$state = {};
module('app', function ($provide) {
$provide.value('$state', $state);
$provide.value('DTColumnBuilder', DTColumnBuilder);
$provide.value('DTOptionsBuilder', DTOptionsBuilder);
});
inject(function ($controller, $injector) {
$scope = $injector.get('$rootScope').$new();
$state = $injector.get('$state');
DTColumnBuilder = $injector.get('DTColumnBuilder');
DTOptionsBuilder = $injector.get('DTOptionsBuilder');
createController = function () {
return $controller('MarketsCtrl', {
$scope: scope,
$state: $state,
DTOptionsBuilder: DTOptionsBuilder,
DTColumnBuilder: DTColumnBuilder
});
}
});
// Stub out the methods of interest.
DTOptionsBuilder.fromFnPromise = angular.noop;
$state.go = function () { console.log('I tried to go but.... I cant!!');
DTColumnBuilder.bar = function () { return 'bar'; };
});
The nature of a spy is that of letting the original implementation do its thing, but recording all of the calls to said function and an assortment of associated data.
A stub on the other hand is a spy with an extended API where you can completely modify the workings of said function. Return value, expected parameters, etc etc.
Assuming we used the aforementioned beforeEach block, DTOptionsBuilder.fromFnPromise would be a noop at this point. As such it would be safe to spy on it and expect the method to have been called.
it('should have been called', function () {
var spy = spyOn(DTOPtionsBuilder, 'fromFnPromise');
createController();
expect(spy).toHaveBeenCalled();
});
If you wanted to manipulate the return value of said function, I would grab sinonjs and make it a stub.
it('became "foo"', function () {
DTOptionsBuilder.fromFnPromise = sinon.stub().returns('foo');
createController();
expect($scope.dtOptions).toEqual('foo');
});
Now, since you are working with promises it's a wee bit more complicated but the basics of stubbing out a promise based function would be to:
Inject $q into your spec file.
Tell the stub to return $q.when(/** value **/) in the case of a resolved promise.
Tell the stub to return $q.reject(/** err **/) in the case of a rejected promise.
Run $timeout.flush() to flush all deferred tasks.
Trigger the done callback to notify Jasmine that you are done waiting for async tasks (may not be needed). This depends on the test framework/runner.
It could look something like so:
it('resolves with "foo"', function (done) {
DTOptionsBuilder.fromFnPromise = sinon.stub().returns($q.when('foo'));
expect($scope.options).to.eventually.become('foo').and.notify(done); // this is taken from the chai-as-promised library, I'm not sure what the Jasmine equivalent would be (if there is one).
createController();
$timeout.flush();
});
Now, a lot of this is just guesswork at this point. It's quite hard to set up a fully working test suite without having the source code running right beside me for cross reference, but I hope this will at least give you some ideas of how to get going with your spies and stubs.
Ok here is the scenario we are trying to unit test with Jasmine. We have a service defined similar to the service below:
(function () {
'use strict';
angular.module('mymodule')
.service('myservice', myservice);
myservice.$inject = ['$q', '$resource', 'progressService',
'myservice2', 'myservice3', 'ngDialog'];
function myservice($q, $resource, progressService,
myservice2, myservice3, ngDialog) {
var self = this;
self.dataListSvc2 = [];
self.dataListSvc3 = [];
self.dataFromResource = null;
self.myRoutine = myRoutine;
var myResource = $resource('/someurl/webapi/GetData');
//TRYING TO TEST THIS ROUTINE!!!
function myRoutine(param1, param2) {
return progressService.show($q.all([
myResource.get({ Param1: param1 }).$promise.then(function (response) {
self.dataFromResource = response;
}),
myRoutine2(param2),
myRoutine3(param2)
]));
}
function myRoutine2(param) {
return myservice2.getSomeData(param).then(function (response) {
var results = [];
self.dataListSvc2 = [];
response.forEach(function (item) {
item.AddField1 = null;
item.AddField2 = false;
results.push(item);
});
self.dataListSvc2 = results;
});
}
function myRoutine3(param) {
return myservice3.getSomeMoreData(param, [6])
.then(function (response) {
self.dataListSvc3 = response;
});
}
}
})();
I am trying to write a unit test for myservice.myRoutine() and not having any luck, I suspect it is due to the $q.all array of promises. Here is a test I settled on but it is not ideal and honestly I don't feel like it's testing anything of value. I have also tried "mocking" the three requests with $httpBackend with no luck as all three responses come back in an array with undefined values. I searched around SO and the web and I am only finding $q.all([]) unit tests referencing controllers but not services. If anyone has some input it would be much appreciated. Here is where I settled for the time being:
describe('My Service: ', function () {
var $httpBackend;
beforeEach(module('mymodule'));
beforeEach(inject(function ($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
}));
it('Can call myroutine from myservice', inject(function (myservice) {
//Arrange
var expectedVal1 = 1234;
var expectedVal2 = 1234;
spyOn(myservice, "myRoutine");
//Act
myservice.myRoutine(expectedVal1, expectedVal2);
//Assert
expect(myservice.myRoutine).toHaveBeenCalled();
}));
});
Use the $httpBackend.when('GET', '/someurl/webapi/GetData').respond(data); to fake the http requests. You should fake the myservice2.getSomeData method too and the myservice3.getSomeMoreData I guess those methods create http request too so you could fake them in the same manner.
expect(myservice.myRoutine).toHaveBeenCalled();
Is irrelevant as you called it manually ;-).
var fixture = {/*some data ... */};
it('Can call myroutine from myservice', inject(function (myservice) {
$httpBackend.when('GET', '/someurl/webapi/GetData').respond(fixture);
//Act
myservice.myRoutine(expectedVal1, expectedVal2);
$rootScope.$apply()
//Assert
expect(myservice.dataFromResource).toBe(fixture);
}));
I am following the angular-test-patterns guide, and I get it working with my first controller test. But when I write the next test, I get the error:
TypeError: 'undefined' is not an object (evaluating '$scope.pages.$promise')
The problem then I know is the following line:
$scope.busy = $scope.pages.$promise;
But I don't know how to deal with this, especially since I am very new in test issues with JavaScript. I looking for a correct and viable way of doing this, to point me in the right direction.
The controller:
angular.module('webvisor')
.controller('page-list-controller', function($scope,Page){
$scope.pages = Page.query();
$scope.busy = $scope.pages.$promise;
});
Service:
angular.module('webvisor').
factory('Page', ['$resource', 'apiRoot', function($resource, apiRoot) {
var apiUrl = apiRoot + 'pages/:id/:action/#';
return $resource(apiUrl,
{id: '#id'},
{update: {method: 'PUT'}}
);
}]);
Test:
'use strict';
describe('Controller: page-list-controller', function () {
var ctrl, scope, rootScope, Page;
beforeEach(function () {
module('webvisor');
module(function ($provide) {
$provide.value('Page', new MockPage());
});
inject(function ($controller, _Page_) {
scope = {};
rootScope = {};
Page = _Page_;
ctrl = $controller('page-list-controller', {
$scope: scope,
$rootScope: rootScope
});
});
});
it('should exist', function () {
expect(!!ctrl).toBe(true);
});
describe('when created', function () {
// Add specs
});
});
Mock:
MockPage = function () {
'use strict';
// Methods
this.query = jasmine.createSpy('query'); // I dont know if this is correct
return this;
};
With Mox, your solution would look like this:
describe('Controller: page-list-controller', function () {
var mockedPages = []; // This can be anything
beforeEach(function () {
mox
.module('webvisor')
.mockServices('Page') // Mock the Page service instead of $httpBackend!
.setupResults(function () {
return {
Page: { query: resourceResult(mockedPages) }
};
})
.run();
createScope();
createController('page-list-controller');
});
it('should get the pages', function () {
expect(this.$scope.pages).toEqual(resourceResult(mockedPages));
});
});
As you see, Mox has abstracted away all those boilerplate injections like $rootScope and $controller. Futhermore there is support for testing resources and promises out of the box.
Improvements
I advise you not to put the resource result directly on the scope, but resolve it as a promise:
$scope.busy = true;
Pages.query().$promise
.then(function (pages) {
$scope.pages = pages;
$scope.busy = false;
});
The Mox test is just this:
expect(this.$scope.busy).toBe(true);
this.$scope.$digest(); // Resolve the promise
expect(this.$scope.pages).toBe(mockedPages);
expect(this.$scope.busy).toBe(false);
NB: You also can store the result of createScope() into a $scope var and reuse that everywhere, instead of accessing this.$scope.
After some research and many try and error cases, I answer myself with a possible solution, but I expect to find some more usable and not too repetitive soon. For now, I am satisfied with this, using $httpBackend.
Test:
'use strict';
describe('Controller: page-list-controller', function () {
var ctrl, scope, rootScope, httpBackend, url;
beforeEach(function () {
module('webvisor');
inject(function ($controller, $httpBackend, apiRoot) {
scope = {};
rootScope = {};
httpBackend = $httpBackend;
url = apiRoot + 'pages/#';
ctrl = $controller('page-list-controller', {
$scope: scope,
$rootScope: rootScope
});
});
});
it('should exist', function () {
expect(!!ctrl).toBe(true);
});
describe('when created', function () {
it('should get pages', function () {
var response = [{ 'name': 'Page1' }, { 'name': 'Page2' }];
httpBackend.expectGET(url).respond(200, response);
httpBackend.flush();
expect(scope.pages.length).toBe(2);
});
});
});
I found this solution reading this question. This work very well, and for now, satisfied me. In future, I tried somethig like those:
angular-easy-test
mox
I have a controller that is calling a service. I want to write my unit tests such that I get coverage on the success and error functions of the then function.
maApp.controller('editEmailAndPasswordController',
["$scope", "emailAndPasswordService",
function editEmailAndPasswordController($scope, emailAndPasswordService) {
$scope.EmailId = 'as#as.com';
$scope.CurrentPassword = '';
$scope.Success = false;
$scope.save = function () {
var request = {
currentPassword: $scope.CurrentPassword,
newEmailId: $scope.EmailId
};
emailAndPasswordService.save(request).then(function (data) {
$scope.Success = true;
}, function (data, status, header, config) {
$scope.Success = false;
});
};
}]);
This is what I have got so for. I want another test for the fail condition as well, but not sure how to set up the mock service.
describe('Controllers', function () {
var $scope, ctrl, controller, svc, def;
describe('editEmailAndPasswordController', function () {
beforeEach(function() {
module('maApp');
});
beforeEach(inject(function ($controller, $rootScope, $q) {
ctrl = $controller;
svc = {
save: function () {
def = $q.defer();
return def.promise;
}
};
spyOn(svc, 'save').andCallThrough();
$scope = $rootScope.$new();
controller = ctrl('editEmailAndPasswordController', { $scope: $scope, emailAndPasswordService: svc });
}));
it('should set ShowEdit as false upon save', function () {
$scope.ShowEdit = true;
$scope.EmailId = 'newEmail';
$scope.CurrentPassword = 'asdf1';
$scope.save();
expect($scope.EmailId).toBe('as#as.com');
expect($scope.Success).toBe(true);
});
});
});
You have some real problems with this code.
Don't call ".andCallThrough()"- that way your test depends on the implementaton of the service and means your controller is not isolated. The main idea is to create unit tests.
svc = {save: jasmine.createSpy()};
svc.save.andReturn(...);
You can't assert against expect($scope.EmailId).toBe('as#as.com'); because you change the value in the code to $scope.EmailId = 'newEmail';
you can create 2 private methods for readability
function success(value) {
var defer = q.defer();
defer.resolve(value);
return defer.promise;
}
function failure(value){
var defer = q.defer();
defer.reject(value);
return defer.promise;
}
Thus in the first test you can call
svc.save.andReturn(success());
$scope.$digest()
expect($scope.Success).toBeTruthy();
And in the other test you will have the same but:
svc.save.andReturn(failure());
$scope.$digest()
expect($scope.Success).toBeFalsy();
In one case, you want the promise to be successful, so you want to resolve the deferred:
$scope.save();
def.resolve('whatever');
$scope.$apply();
expect($scope.Success).toBe(true);
...
In the other case, you want the promise to be a failure, so uou want to reject the deferred:
$scope.save();
def.reject('whatever');
$scope.$apply();
expect($scope.Success).toBe(false);
...
This is explained in the documentation.