Unit Testing AngularJS route with "resolve" - angularjs

I am trying to unit test one of my routes and I get the infamous "Error: Unexpected request" error. My route takes in a "resolve" parameter and looks like this:
when('/Users',
{
templateUrl: 'app/users/user-list.tpl.html',
controller: 'UserListCtrl',
resolve: {
userAccounts: ['UserAccounts', function(UserAccounts) {
return UserAccounts.query({ id: 123 });
}]
}
})
where UserAccounts is a "resource". I am testing my route as follows:
beforeEach(inject(function (_$route_, _$location_, _$rootScope_, _$httpBackend_) {
$route = _$route_;
$location = _$location_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', 'api/Accounts/123/UserAccounts').respond([]);
$httpBackend.when('GET', 'app/users/user-list.tpl.html').respond({});
}));
it('/Users should get user-list template and use UserListCtrl', inject(function($controller) {
$httpBackend.expectGET('app/users/user-list.tpl.html');
$httpBackend.expectGET('api/Accounts/123/UserAccounts');
expect($route.current).toBeUndefined();
$location.path('/Users');
$rootScope.$digest();
expect($route.current.loadedTemplateUrl).toBe('app/users/user-list.tpl.html');
expect($route.current.controller).toBe('UserListCtrl');
}));
And the test fails with Error: Unexpected request: GET http://myserver/api/Accounts/123/UserAccounts. This is the get that my resolve is calling. Any ideas what the right way is to test such a route?

Two things look strange about your test code.
First, your beforeEach method has the GET requests duplicated with those being called in the actual test. Try removing them.
Second, it looks as if you are missing a call to $httpBacken.flush() before your final assertions.
Hopefully this helps.

You can try mocking the service
beforeEach(module(function($provide) {
$provide.value('UserAccounts', {
query: angular.noop
});
}));
If you care whether the service method has been called, you can create a spy.
beforeEach(function() {
spyOn(UserAccounts, 'query');
});

Wow! this was a silly one. I had the complete URL defined in my Resource
http://myserver/api/Accounts/:accountId/UserAccounts/:userId
And I was setting up expectation on my $httpBackend to check for relative paths.
api/Accounts/123/UserAccounts

To unit test resolved values of a route:
var resolvedUserAccounts = $injector.invoke($route.current.$$route.resolve.userAccounts);

Related

Unit Test on $state in function

I have a function that triggers to change page. How can I unit test this? I'm always getting a failed result saying:
Expected ' ' to be 'add'
So the current state name is still the home page
Function
$scope.goToAddVote = function(){
$state.go('add');
}
Unit Testing
it('should redirect index.html to add.html after click on button', inject(function($state) {
scope.goToAddVote();
$state.go('add');
expect($state.current.name).toBe('add');
}));
EDIT: Using Nilo's answer
var mockStateService = {
go: jasmine.createSpy('add')
};
it('should redirect index.html to add.html after click on button', inject(function($state) {
scope.goToAddVote();
$state.go('add');
expect(mockStateService.go).toHaveBeenCalledWith('add');
}));
Your unit test is failing because the state you are trying to recieve is never registered in the first place. Thats why the go function will have no effect which ends in your current state-name being ''
What you should do ist inject a mock of the $state service with a jasmine-spy as a replacement for the go-function. In your assertion you then expect this function to have been called.
This is way cleaner in terms of unit-testing, because you do not also test the capabilities of $state.
The mocked service would look something along the lines of this:
var mockStateService = {
go: jasmine.createSpy()
}
The assertion then should look like this:
expect(mockStateService.go).toHaveBeenCalledWith('add');
EDIT: Your result could also be caused by the fact mentioned by Pankaj Parkar. So you can also try that, but either why you should mock the service so you really only test the code you have written yourself.
EDIT2: You are not injecting the mocked service, but still using the original $state
var mockStateService,
myScope,
ctrl;
beforeEach(inject(function($controller, $rootScope) {
mockStateService = {
go: jasmine.createSpy()
}
myScope = $rootScope.$new();
ctrl = $controller('myController', {
$scope: myScope,
$state: mockStateService
});
}));
// tip: write better descriptions, what you are writing is not what's really happening
it('should redirect index.html to add.html after click on button', function() {
myScope.goToAddVote();
expect(mockStateService.go).toHaveBeenCalledWith('add');
});

How to test an error response in angular's $httpBackend?

We are using angular 1.2.x (we have to due to IE8). We are testing with Karma and Jasmine. I want to test the behavior of my modules, in case the server responds with an error. According to the angular documentation, I should just simply prepare the $httpBackend mock like this (exactly as I'd expect):
authRequestHandler = $httpBackend.when('GET', '/auth.py');
// Notice how you can change the response even after it was set
authRequestHandler.respond(401, '');
This is what I am doing in my test:
beforeEach(inject(function($injector) {
keepSessionAliveService = $injector.get('keepSessionAliveService');
$httpBackend = $injector.get('$httpBackend');
$interval = $injector.get('$interval');
}));
(...)
describe('rejected keep alive request', function() {
beforeEach(function() {
spyOn(authStorageMock, 'get');
spyOn(authStorageMock, 'set');
$httpBackend.when('POST', keepAliveUrl).respond(500, '');
keepSessionAliveService.start('sessionId');
$interval.flush(90*60*1001);
$httpBackend.flush();
});
it('should not add the session id to the storage', function() {
expect(authStorageMock.set).not.toHaveBeenCalled();
});
});
But the test fails, because the mock function is being called and I can see in the code coverage that it never runs into the error function I pass to the §promise.then as second argument.
Apparently I am doing something wrong here. Could it have to with the older angular version we're using?
Any help would be appreciated!
Something like this:
it("should receive an Ajax error", function() {
spyOn($, "ajax").andCallFake(function(e) {
e.error({});
});
var callbacks = {
displayErrorMessage : jasmine.createSpy()
};
sendRequest(callbacks, configuration);
expect(callbacks.displayErrorMessage).toHaveBeenCalled();

AngularJS "No pending request to flush" while unit testing a controller with a $resource

I am writing unit tests for a controller. This controller has a $resource service injected :
function controller($scope, Service) {
Service.get(function(result){
// do stuff with the result, not relevant here
}
}
The service is defined like this :
angular.module('so').factory('Service', ['$resource', service]);
function service($resource) {
return $resource('/url', null, {
get: { method: 'POST', params: {}, isArray: false}
});
}
My Jasmine unit test is the following :
describe("Controller", function(){
var $httpBackend;
beforeEach(function() {
module('so');
inject(function( _$httpBackend_) {
$httpBackend = _$httpBackend_;
});
});
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
});
I get an error when running the test :
Error: No pending request to flush ! in file:///path/to/angular-mock.js (line 1453)
I added a console.log() in the callback from Service.get() and indeed, it is not called (everything outside of the callback is of course called). Also tried to add a scope digest if not phased after controller creation in the unit test, as I saw suggested in an other question, with no luck.
I know that I can mock that in some other ways, but using $httpBackend seems the perfect solution for the test : mocking the webserver and the data received.
I'm using AngularJS 1.2.16 (can't upgrade to 1.3.*, IE 8 compatibility required). I first used 1.2.13 and updated to check if it would solve the issue, without any luck.
That was an injection issue that was solved by changing the test from
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
To:
it('should have done stuff irrelevant to the question', inject(function(Service) {
// edited lines because they did not change
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
So basicaly, adding the inject() in the test function and passing the service to the controller "manually".
I found the issue, that's great, but I don't really understand why it doesn't work. Also, I tried this right after finding the solution :
it('should have done stuff irrelevant to the question', inject(function() {
// edited lines because they did not change
var Service = $injector.get('Service'),
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
but this fail again, with the same "no pending request" error. I'm guessing that's some sort of racing issue, where my service can't get the proper $httpBackend to be injected when it's created afterwards, but I don't really understand why this occurs. If anybody can enlighten me... I'll be grateful.

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

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.

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