Angularjs and Jasmine: Testing a controller with a service making ajax call - angularjs

I am very new to testing javascript. My application is using angularjs. I am using jasmine as a testing framework.
Here is the controller I am testing:
angular.module('logonController', ["ngval", "accountFactory"])
.controller("logonController", function logOnController(accountFactory, $scope, $window) {
$scope.hasServerError = false;
$scope.Logon = function () {
accountFactory.Logon($scope.data.LogOnModel)
.then(function (data) {
$window.location.href = "/";
},
function (data) {
$scope.hasServerError = true;
});
}
})
where accountFactory.Logon is making a Post request to the server.
What I want to test is when calling accountFactory.Logon:
On success - window.location.href is called
On error $scope.hasServerError is set to true
So far I have managed to do this:
"use strict";
describe("Logon Controller", function () {
var $scope, $location, $rootScope, $httpBackend, $controller, $window, createController;
beforeEach(function () {
module("logonController");
});
beforeEach(inject(function ($injector) {
$rootScope = $injector.get("$rootScope");
$scope = $rootScope.$new();
$location = $injector.get("$location");
$httpBackend = $injector.get("$httpBackend");
$controller = $injector.get("$controller");
$window = $injector.get("$window");
}));
beforeEach(function () {
createController = function () {
return $controller("logonController", {
"$scope": $scope,
});
};
$scope.data = {
LogOnModel: { username: "user", password: "pass" }
};
$window = { location: { href: jasmine.createSpy() } };
});
it("should redirect on successfull login", function () {
var controller = createController();
$httpBackend.whenPOST("/Account/Logon").respond(function (method, url, data, headers) {
return [200, {}, {}];
});
$scope.Logon();
$httpBackend.flush();
expect($window.location.href).toHaveBeenCalled();
});
});
My idea is to create a spy on $window.location.href and only check if it is called. But I am getting
Expected spy unknown to have been called.
As I said I am very new to testing javascript, so any help will be appreciated.

Sten Muchow's Answer is wrong in several aspects:
you can't specify a compound property name ("location.href") as 2nd parameter to spyOn. You have to give a real property name.
And even if you would do the spyOn correctly, andCallThrough() would still raise an exception, as $window.location.href is not a function which could be called through.
But he is still right in saying that you should not intermingle your controller test with the service test.
To answer the question:
The reason, that your expectation is not met (that even the spy still exists*) is, that you're doing the $window.location.href assignment inside a promise's then() function. That means, it will be executed asynchronously, namely AFTER your expect() call. To go around this, you would need to make your test work asynchronously (for how to do this I would like to advise you to the Jasmine documentation: http://jasmine.github.io/2.0/introduction.html).
* In accountFactory.Logon, by doing $window.location.href = (i.e. assignment) you will effectively overwrite your spy.
Even better solution:
Instead of manipulating $window.location.href, you should use $location.url().
$location is an Angular core service. You will benefit from the integration within the Angular application lifecycle (i.e. watchers will be automatically processed when the url changes) + it is seamlessly integrated with existing HTML5 APIs like History API: https://docs.angularjs.org/guide/$location
Then, you can spy on $location.url() as you would have spied on $window.location.href (if it had been a function).

You need to create a spy:
spyOn($window, 'location.href').andCallThrough();
But on a bigger note though, you shouldnt be testing the functionality of your service in the controller test.

Related

Spying on service dependencies

I'm curious about the best way to spy on dependencies so I can make sure that their methods are being called in my services. I reduced my code to focus on the problem at hand. I'm able to test my service fine, but I want to also be able to confirm that my service (In this case metricService) has methods that are also being called. I know I have to use createSpyObj in some way, but while the function is executing properly, the spyObj methods are not being caught. Should I even be using createSpyObj? Or should I use spyObj? I'm a but confused about the concept of spying when it concerns dependencies.
UPDATE: When using SpyOn I can see one method getting called, but other methods are not
Test.spec
describe("Catalogs service", function() {
beforeEach(angular.mock.module("photonServicesCommons"));
var utilityService, metricsService, loggerService, catalogService, localStorageService;
var $httpBackend, $q, $scope;
beforeEach(
inject(function(
_catalogService_,
_metricsService_,
_$rootScope_,
_$httpBackend_
) {
catalogService = _catalogService_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/ctrl/catalog/all-apps").respond(
{
catalogs: catalogs2
}
);
metricsService = _metricsService_;
startScope = spyOn(metricsService, 'startScope')
emitSuccess = spyOn(metricsService, 'emitGetCatalogSuccess').and.callThrough();
endScope = spyOn(metricsService, 'endScope');
})
);
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
promise3.then(function (res) {
expect(res.catalogs).toEqual(catalogs2);
});
expect(metricsService.startScope).toHaveBeenCalled();
expect(metricsService.emitGetCatalogSuccess).toHaveBeenCalled();
expect(metricsService.endScope).toHaveBeenCalled();
$scope.$digest();
done();
$httpBackend.flush();
});
});
});
Service
public getCatalog(): IPromise<Interfaces.CatalogsResponse> {
if (this.response !== null) {
let finalResponse:any = angular.copy(this.response);
return this.$q.when(finalResponse);
}
return this.$q((resolve, reject) => {
this.metricsService.startScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
this.$http.get(this.catalogEndpoint).then( (response) => {
let data: Interfaces.CatalogsResponse = response.data;
let catalogs = data.catalogs;
if (typeof(catalogs)) { // truthy check
catalogs.forEach((catalog: ICatalog) => {
catalog.applications.forEach((application: IPhotonApplication) => {
if( !application.appId ) {
application.appId = this.utilityService.generateUUID();
}
})
});
} else {
this.loggerService.error(this.TAG, "Got an empty catalog.");
}
this.response = data;
this.metricsService.emitGetCatalogSuccess();
console.log("CALLING END SCOPE");
this.metricsService.endScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
resolve(finalResponse);
}).catch((data) => {
this.loggerService.error(this.TAG, "Error getting apps: " + data);
this.response = null;
this.metricsService.emitGetCatalogFailure();
reject(data);
});
});
} // end of getCatalog()
Instead of using createSpyObj, you can just use spyOn. As in:
beforeEach(
inject(function(
_catalogService_,
_$rootScope_,
_$httpBackend_,
_metricsService_ //get the dependecy from the injector & then spy on it's properties
) {
catalogService = _catalogService_;
metricsService = _metricsService_;
$scope = _$rootScope_.$new();
...
// create the spy object for easy referral later on
someMethodSpy = jasmine.spyOn(metricsService, "someMethodIWannaSpyOn")
})
);
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
...other expects
...
//do the spy-related expectations on the function spy object
$httpBackend.flush(); // this causes the $http.get() to "move forward"
// and execution moves into the .then callback of the request.
expect(someMethodSpy).toHaveBeenCalled();
});
});
I've used this pattern when testing complex angular apps, along with wrapping external imported/global dependencies in angular service wrappers to allow spying and mocking them for testing.
The reason that createSpyObject won't work here is that using it will create a completely new object called metricService with the spy props specified. It won't be the same "metricService" that is injected into the service being tested by the angular injector. You want to get the actual same singleton service object from the injector and then spy on it's properties.
The other source of dysfunction was the $httpBackend.flush()s location. The $httpBackend is a mock for the $http service: you pre-define any number of expected HTTP requests to be made by the code you are testing. Then, when you call the function that internally uses $http to make a request to some url, the $httpBackend instead intercepts the call to $http method (and can do things like validate the request payload and headers, and respond).
The $http call's then/error handlers are only called after the test code calls $httpBackend.flush(). This allows you to do any kind of setup necessary to prep some test state, and only then trigger the .then handler and continue execution of the async logic.
For me personally this same thing happens every single time I write tests with $httpBackend, and it always takes a while to figure out or remember :)

How does the createSpy work in Angular + Jasmine?

I made a simple demo of a factory and I am trying to test this using jasmine. I am able to run the test but I am using the spyOn method. I would rather use jasmine.createSpy or jasmine.createSpyObj to do the same test. Could someone help me to refactor my code so that uses these methods instead in my example?
http://plnkr.co/edit/zdfYdtWbnQz22nEbl6V8?p=preview
describe('value check',function(){
var $scope,
ctrl,
fac;
beforeEach(function(){
module('app');
});
beforeEach(inject(function($rootScope,$controller,appfactory) {
$scope = $rootScope.$new();
ctrl = $controller('cntrl', {$scope: $scope});
fac=appfactory;
spyOn(fac, 'setValue');
fac.setValue('test abc');
}));
it('test true value',function(){
expect(true).toBeTruthy()
})
it('check message value',function(){
expect($scope.message).toEqual(fac.getValue())
})
it("tracks that the spy was called", function() {
expect(fac.setValue).toHaveBeenCalled();
});
it("tracks all the arguments of its calls", function() {
expect(fac.setValue).toHaveBeenCalledWith('test abc');
});
})
update
angular.module('app',[]).factory('appfactory',function(){
var data;
var obj={};
obj.getValue=getValue;
obj.setValue=setValue;
return obj;
function getValue(){
return data;
}
function setValue(datavalue){
data=datavalue;
}
}).controller('cntrl',function($scope,appfactory){
appfactory.setValue('test abc');
$scope.message=appfactory.getValue()
})
I have changed your plunkr:
spy = jasmine.createSpy('spy');
fac.setValue = spy;
Edit
In Jasmine, mocks are referred to as spies. There are two ways to
create a spy in Jasmine: spyOn() can only be used when the method
already exists on the object, whereas jasmine.createSpy() will return
a brand new function.
Found the information here. The link has a lot more information about creating spies.
As said in the comments, you have absolutely no need for spies to test such a service. If you had to write the documentation for your service: you would say:
setValue() allows storing a value. This value can then be retrieved by calling getValue().
And that's what you should test:
describe('appfactory service',function(){
var appfactory;
beforeEach(module('app'));
beforeEach(inject(function(_appfactory_) {
appfactory = _appfactory_;
}));
it('should store a value and give it back',function() {
var value = 'foo';
appfactory.setValue(value);
expect(appfactory.getValue()).toBe(value);
});
});
Also, your service is not a factory. A factory is an object that is used to create things. Your service doesn't create anything. It is registered in the angular module using a factory function. But the service itself is not a factory.

How to make a REST API call in a controller Unit test?

I am trying to make a real call and Assign Scopes for testing
Using passThrough Method but Throwing Error
Code Follows:-
describe('Controller: MainCtrl', function () {
// load the controller's module
beforeEach(module('w00App'));
var scope, MainCtrl, $httpBackend;
// Initialize the controller and a mock scope
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('http://api.some.com/testdata').passThrough();
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
})); it('should make a post to refresh the friends list and return matching users', function(){
var deferredResponse = $httpBackend.expectGET('http://api.some.com/testdata').passThrough();
console.log('response'+JSON.stringidy(deferredResponse));
$httpBackend.flush();
// expect(deferredResponse).toEqual(deferredResponse);
}); });
Error :- TypeError: 'undefined' is not a function (near '...
').passThrough();...') .....
How can i call and Assign Scopes Like in Real controller ? pls Help.. it make my life Easy .
When testing a real controller and inside the controller you make some REST calls to the backed, it is best to mock those response calls, intercept the calls via $httpBackend object.
jasmine.getJSONFixtures().fixturesPath = 'base/test/unit/authz/api_mock/';
$httpBackend.when('POST', CONFIG.get('MAIN_URL_FOR_REST_SERVICES') + 'actions/search').respond(function() {
return [200, window.getJSONFixture('actions.json')];
});
at least, this is how I proceed in testing the controllers.
if you really really want to call the backed use:
$http.get(YOUR_URL).success(function(data) {
--- your test ---
});
and do not forget do inject the http service in the beforeEach method:
beforeEach(inject(function(_$http_) {
$http = _$http_;
}));

How to test a service function in Angular that returns an $http request

I have a simple service that makes an $http request
angular.module('rootApp')
.factory('projectService', ['$http', function ($http) {
return {
getProject: getProject,
getProjects: getProjects,
};
function getProject(id) {
return $http.get('/projects.json/', { 'params': { 'id': id }});
}
}]);
I'm wondering how can I test this simply and cleanly? Here's what I have so far in my test.
describe("Root App", function () {
var mockGetProjectResponse = null,
$httpBackend = null;
beforeEach(module('rootApp'));
beforeEach(inject(function (_$httpBackend_, projectService) {
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', '/projects.json/?id=1').response(mockGetProjectResponse);
}));
describe("should get projects successfully", inject(function (projectService) {
it("should return project", function () {
// I essentially want to do something like this (I know this isn't the right format).. but:
//expect(projectService.getProject(1)).toBe(mockGetProjectResponse);
});
}));
I want to avoid explicitly calling $http.get(...) in my test, and rather stick to calling the service function, i.e. projectService.getProject(1). What I'm stuck on is not being able to do something like this:
projectService.getProject(1)
.success(function (data) {
expect(data).toBe(whatever);
})
.error(function () {
});
Since there's 'no room' to call $httpBackend.flush();
Any help would be much appreciated. Thanks.
The usual recipe for testing promises (including $http) is
it("should return project", function () {
var resolve = jasmine.createSpy('resolve');
projectService.getProject(1).then(resolve);
expect(resolve).toHaveBeenCalledWith(mockGetProjectResponse);
});
A good alternative is jasmine-promise-matchers which eliminates the need for spy boilerplate code.
Here's a plunker that demonstrates both of them.
Generally the one may want to keep the methods that make $http calls as thin as possible and stub them instead, so mocking $httpBackend may not be required at all.
In current example the spec tests literally nothing and can be omitted and left to e2e tests if the code coverage isn't an end in itself.

Mocking a resolve in $dialog

I've been creating an angularjs framework for an application I'm planning to write. At the moment I'm working on a sample application, I'm documenting as I go in a tutorial so that I have everything I did in one place.
I'm currently trying to create unit tests using karma and jasmine for the modal dialog I'm presenting. This modal dialog is created using the $dialog service from angular-bootstrap. This dialog I think is using a promise to pass data into the dialog controller, and I'd like to resolve that promise so I can check in my unit test that the data that has been passed in is as expected. I'm having a little difficulty in working out how to resolve that, I see examples using either scope.$apply or scope.$digest, neither appear to work and to be frank I don't quite understand what it's doing. I'm concerned that in the unit test I have assigned this promise to a variable, and perhaps that it won't resolve once assigned to a variable. I see mention that this "resolve" parameter is similar to the resolve on a route, but so far that hasn't helped me, and I'm not 100% sure that it's really a promise at all.
I'm looking both for something that makes it work, but also an explanation of why that works.
The controller I'm seeking to test looks like this:
.controller( 'ClubCtrl', function ClubController( $scope, ClubRes, $dialog ) {
$scope.clubs = ClubRes.query();
/* this is called from a button, which passes one of the clubs from $scope.clubs */
$scope.editClub = function(club) {
$scope.myDialog = $dialog.dialog({dialogFade: false, resolve: {club: function(){return angular.copy(club);}}});
$scope.myDialog.open('club/club_edit.tpl.html', 'ClubEditCtrl').then(function(result){
if (result === 'cancel'){}
else {
$scope.clubs = ClubRes.query();
}
});
};
})
The unit test I'm trying to get working at this point is aiming to mock out the whole dialog, and to check that the dialog has been called with the correct input parameters:
describe( 'Base club controller', function() {
var scope, httpBackend;
//mock Application to allow us to inject our own dependencies
beforeEach(angular.mock.module('league'));
//mock the controller for the same reason and include $rootScope and $controller
beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_ ){
//create an empty scope
scope = $rootScope.$new();
// setup a mock for the resource - instead of calling the server always return a pre-canned response
httpBackend = _$httpBackend_;
httpBackend.when('GET', '../clubs.json').respond([
{"contact_officer":"Officer 1","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":1,"name":"Club 1","updated_at":"2012-03-03T00:00:00Z"},
{"contact_officer":"Officer 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Club 2","updated_at":"2012-03-03T00:00:00Z"}]);
// setup a mock for the dialog - when called it returns the value that was input when it was instantiated
scope.fakeDialog = {
parameters: null,
response: null,
template: null,
controller: null,
dialog: function(parameters) {
this.parameters = parameters;
return this;
},
open: function(template, controller) {
this.template = template;
this.controller = controller;
return this;
},
then: function(callBack){
callBack(this.response);
}
};
//declare the controller and inject our empty scope
$controller('ClubCtrl', {$scope: scope, $dialog: scope.fakeDialog});
}));
it('Calls edit on first row', function() {
// check nothing set beforehand
expect(scope.fakeDialog.parameters).toBe(null);
expect(scope.fakeDialog.template).toBe(null);
expect(scope.fakeDialog.controller).toBe(null);
// call edit
scope.editClub(scope.clubs[0]);
scope.$digest();
httpBackend.flush();
// expect stuff to have happened
expect(scope.fakeDialog.parameters.club.name).toBe('Club 1');
expect(scope.fakeDialog.template).toBe('club/club_edit.tpl.html');
expect(scope.fakeDialog.controller).toBe('ClubEditCtrl');
});
});
What I'm actually getting in console.log(scope.fakeDialog.parameters) is:
Object{dialogFade: false, resolve: Object{club: function (){ ... }}}
So my club is buried inside "resolve: Object......", which I think is a promise. I think what I need is a way to trigger that to resolve - but I'm not sure what that is.
OK, no responses as yet, and I've had the time tonight to piece through it slowly.
The short answer is that the resolve parameter to a dialog isn't necessarily a promise (although I think it can be sometimes if you wish it to be). Since I haven't passed in a promise I can directly evaluate these functions to work out their results, although I thought I'd tried that before and it didn't work.
I've also spent some time looking at spyOn, and I can use that for some of the things I had my mock doing, so I'm tidying that up at the same time.
My working code is as follows. Firstly, the controller that's being tested:
.controller( 'ClubCtrl', function ClubController( $scope, ClubRes, $dialog ) {
$scope.clubs = ClubRes.query();
/* this is called from a button, which passes one of the clubs from $scope.clubs */
$scope.editClub = function(club) {
$scope.myDialog = $dialog.dialog({dialogFade: false, resolve: {club: function(){return angular.copy(club);}}});
$scope.myDialog.open('club/club_edit.tpl.html', 'ClubEditCtrl').then(function(result){
if (result === 'cancel'){}
else {
$scope.clubs = ClubRes.query();
}
});
};
})
Then, the test code that tests that:
describe( 'Base club controller', function() {
var scope, httpBackend;
//mock Application to allow us to inject our own dependencies
beforeEach(angular.mock.module('league'));
//mock the controller for the same reason and include $rootScope and $controller
beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_ ){
//create an empty scope
scope = $rootScope.$new();
// setup a mock for the resource - instead of calling the server always return a pre-canned response
httpBackend = _$httpBackend_;
httpBackend.when('GET', '../clubs.json').respond([
{"contact_officer":"Officer 1","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":1,"name":"Club 1","updated_at":"2012-03-03T00:00:00Z"},
{"contact_officer":"Officer 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Club 2","updated_at":"2012-03-03T00:00:00Z"}]);
// setup a mock for the dialog - when called it returns the value that was input when it was instantiated
scope.fakeDialog = {
response: null,
club: null,
dialog: function(parameters) {
this.club = parameters.resolve.club();
return this;
},
open: function(template, controller) {
return this;
},
then: function(callBack){
callBack(this.response);
}
};
//declare the controller and inject our empty scope
$controller('ClubCtrl', {$scope: scope, $dialog: scope.fakeDialog});
}));
it('Calls edit on first row', function() {
// we expect the fakeDialog dialog and open methods to be called, so we spy on them to get the parameters
spyOn(scope.fakeDialog, "dialog").andCallThrough();
spyOn(scope.fakeDialog, "open").andCallThrough();
// call edit
scope.editClub(scope.clubs[0]);
scope.$digest();
httpBackend.flush();
// check parameters passed in
expect(scope.fakeDialog.dialog).toHaveBeenCalledWith({dialogFade: false, resolve: {club: jasmine.any(Function)}});
expect(scope.fakeDialog.club.contact_officer).toEqual('Contact Officer 1');
expect(scope.fakeDialog.open).toHaveBeenCalledWith('club/club_edit.tpl.html', 'ClubEditCtrl');
});
});
This seems to call the function and give the response into the club property on the fakeDialog object.

Resources