AngularJS Jasmine testing init functions - angularjs

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.

Related

Angular ui-route testing using mocha chai and sinon

I need to test below code in angularjs using mocha chai and sinon
$scope.send = function() {
$state.transitionTo('module.sendhome');
};
Below is test case for the same
it('send' , function () {
scope.send();
});
on running the above test case getting error as given below.
Error: No such state 'module.sendhome'
In my test case need to check if $state.transitionTo is called with parameter module.sendhome.
You need to stub out $state and the transitionTo method and write expectations on that. This will keep your unit test clean and flexible, so as to not trigger the real implementation of $state.transitionTo (which in turn triggers the error you are experiencing).
var $scope, $state;
beforeEach(function () {
$state = {};
module('your_module', function ($provide) {
$provide.value('$state', $state);
});
inject(function ($injector, $controller) {
$state = $injector.get('$state');
$scope = $injector.get('$rootScope').$new();
$controller('your_controller', {
$scope: $scope,
$state: $state
});
});
// Stub API
$state.transitionTo = sinon.stub();
});
it('calls the transitionTo method', function () {
$scope.send();
expect($state.transitionTo).to
.have.been.calledOnce
.and.calledWith('module.sendhome');
});
Edit
As per the notion of not stubbing out things we do not own (which, I don't fully agree on but for the sake of argument let's say I do).
Don't stub $state.transitionTo, but rather spy on it.
Now - you will have to register a state matching that of your expectation in order for $state.transitionTo to not crash.
var stateProvider;
beforeEach(function () {
module('ui.router', function ($stateProvider) {
stateProvider = $stateProvider;
});
/** The rest of your beforeEach block **/
stateProvider.state('module.sendhome', {});
});
And then in your it:
it('calls the transitionTo method with the correct params', function () {
var spy = sinon.spy($state, 'transitionTo');
$scope.send();
expect(spy).to
.have.been.calledOnce
.and.calledWith('module.sendhome');
});
Edit#2
If you want to ensure that you ended up on the correct state after invoking your $scope method, I would look into this awesomely awesome stateMock.
Inject stateMock as another module prior to your own and write expectations such as:
afterEach(function () {
$state.ensureAllTransitionsHappened();
});
it('should travel to the correct state', function () {
$state.expectTransitionTo('module.sendhome');
$scope.send();
});

Testing an Angular Controller that uses Datatables - mocking DTOptionsBuilder and DT ColumnBuilder

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.

Jasmine doesn't execute callbacks from $resource

So I'm having this issue when writing my tests that I don't know how to solve:
This is my controller:
'use strict';
angular.module('testApp')
.controller('SettingsExtrasCtrl', function ($scope, $log, Auth, Property, $modal, dialogs, growl) {
$scope.deleteExtra = function(index) {
var dlg = dialogs.confirm('Please Confirm', 'Are you sure you want to delete '+$scope.selectedProperty.extras[index].name+'?');
dlg.result.then(function() {
Property.removeExtra({ _id : $scope.selectedProperty._id, otherId : $scope.selectedProperty.extras[index]._id }, function(res) {
$scope.selectedProperty.extras.splice(index,1);
growl.success("Success message", {title : 'Success'});
},
function(err) {
console.log(err);
});
});
};
});
$scope.selectedProperty comes from a parent controller.
And here is my test:
'use strict';
describe('Controller: SettingsExtrasCtrl', function () {
// load the controller's module
beforeEach(module('testApp'));
var SettingsExtrasCtrl, scope, stateParams, Property, httpBackend;
var dialogs = {
confirm: function (title, message) {
return {
result: {
then: function (callback) {
return callback();
}
}
}
}
};
var fakeProperty = {
_id : 'propertyId',
extras : [
{
_id : 'extraId',
name : 'Extra'
}
]
};
beforeEach(inject(function ($controller, $rootScope, _Property_, _$httpBackend_, $state, $modal, _dialogs_) {
scope = $rootScope.$new();
scope.selectedProperty = fakeProperty;
stateParams = {propertyId: fakeProperty._id};
Property = _Property_;
httpBackend = _$httpBackend_;
spyOn(Property, 'removeExtra');
spyOn(_dialogs_, 'confirm').andCallFake(dialogs.confirm);
SettingsExtrasCtrl = $controller('SettingsExtrasCtrl', {
$scope: scope,
$stateParams: stateParams,
dialogs: _dialogs_,
$state: $state
});
}));
it('should delete an extra', inject(function(_dialogs_) {
httpBackend.expectDELETE('/api/properties/' + stateParams.propertyId + '/extras/someextraId').respond(200, '');
scope.deleteExtra(0);
expect(_dialogs_.confirm).toHaveBeenCalled();
expect(Property.removeExtra).toHaveBeenCalled();
expect(scope.selectedProperty.extras.length).toBe(0);
}));
});
The assert expect(scope.selectedProperty.extras.length).toBe(0); fails because expects 1 to be 0 because the success callback from Property.removeExtra is never called.
Any idea on how to solve this?
Thanks.
For promise to be executed you have to call a digest cycle :
scope.deleteExtra(0);
scope.$digest();
[EDIT]
Has it's a network call, you will have to look at $httpBackend
basically it work like that :
//you can mock the network call
$httpBackend.whenGET('https://url').respond(200);//status code or object
// stuff that make the call
....
$httpBackend.flush();
expect(thing).toBe(stuff);
A bit of doc :
The $httpBackend used in production always responds to requests asynchronously. If we preserved this behavior in unit testing, we'd have to create async unit tests, which are hard to write, to follow and to maintain. But neither can the testing mock respond synchronously; that would change the execution of the code under test. For this reason, the mock $httpBackend has a flush() method, which allows the test to explicitly flush pending requests. This preserves the async api of the backend, while allowing the test to execute synchronously.

jasmine testing controller calling service from scope function

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);
});

How do I test a controller that watches for changes on an injected service?

I'm using a service to share data between controllers. If a value on the service changes, I want to update some data binding on my controllers. To do this, I'm using $scope.$watchCollection (because the value I'm watching is a simple array). I'm having trouble trying to figure out how to test this in Jasmine + Karma.
Here is a simple Controller + Service setup similar to what I'm doing in my app (but very simplified):
var app = angular.module('myApp', []);
// A Controller that depends on 'someService'
app.controller('MainCtrl', function($scope, someService) {
$scope.hasStuff = false;
// Watch someService.someValues for changes and do stuff.
$scope.$watchCollection(function(){
return someService.someValues;
}, function (){
if(someService.someValues.length > 0){
$scope.hasStuff = false;
} else {
$scope.hasStuff = true;
}
});
});
// A simple service potentially used in many controllers
app.factory('someService', function ($timeout, $q){
return {
someValues: []
};
});
And here is a test case that I've attempted (but does not work):
describe('Testing a controller and service', function() {
var $scope, ctrl;
var mockSomeService = {
someValues : []
};
beforeEach(function (){
module('myApp');
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope,
someService: mockSomeService
});
});
});
it('should update hasStuff when someService.someValues is changed', function (){
expect($scope.hasStuff).toEqual(false);
// Add an item to someService.someValues
someService.someValues.push(1);
//$apply the change to trigger the $watch.
$scope.$apply();
//assert
expect($scope.hasStuff).toEqual(true);
});
});
I guess my question is twofold:
How do I properly mock the service that is used in the controller?
How do I then test that the $watchCollection function is working properly?
Here is a plunkr for the above code. http://plnkr.co/edit/C1O2iO
Your test (or your code ) is not correct .
http://plnkr.co/edit/uhSdk6hvcHI2cWKBgj1y?p=preview
mockSomeService.someValues.push(1); // instead of someService.someValues.push(1);
and
if(someService.someValues.length > 0){
$scope.hasStuff = true;
} else {
$scope.hasStuff = false;
}
or your expectation makes no sense
I strongly encourage you to lint your javascript (jslint/eslint/jshint) to spot stupid errors like the first one.Or you'll have a painfull experience in writing javascript. jslint would have detected that the variable you were using didnt exist in the scope.

Resources