Testing controller that depends on a service which is based on resource - angularjs

I have a controller that calls a Service which is a wrapper for a Resource. Like this:
app.factory("Service", ["$resource", function ($resource) {
return $resource("/api/Service/get");
}]);
Return value of the service's method is assigned to a variable within the controller. Normally, the variable is of type Resource and it contains a promise. When the promise is resolved, the variable is populated with all values returned from the backend.
I track then on the promise in order to modify the model received from the backend. Like so:
this.model = Service.get();
this.model.$promise.then(function(data) {
// do something with data
});
I need to test the value of the resulting model variable in my controller.
The only way I found to do this, is to use $httpBackend with a real implementation of my Service. However, this is ugly because then, testing my controller, I have to pass request path "api/Service/get" to the httpBackend.when() in order for it to respond with some value.
An excerpt form my test:
// call Controller
$httpBackend.when('GET', '/api/Service/get').respond(someData);
$httpBackend.flush();
expect(scope.model.property).toBe(null);
This seems and feels utterly wrong. The whole point of using a separate service to deal with resource is for the controller to not know anything about the url and http method name. So what should I do?
In other words, what I want to test is that then gets called and does what I need it to do.
I guess I could probably create a separate service that gets called in then and do what I need to do with the model but it feels a bit overkill if all I want to do is, for example, set one field to null depending on a simple condition.

You are correct, you shouldn't have to use $httpBackend unless you are using $http in the controller you are testing.
As you wrote, the controller shouldn't need to know anything about the implementation of Service. What the controller knows is that Service has a get method that returns an object with a $promise property that is a promise.
What you want to do is to use a fake implementation of Service in your test. There are multiple ways to do this via mocks, spies, stubs etc, depending on your use case and which testing framework(s) you are using.
One way is to create a fake implementation like this:
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
You want to be able to access deferred from the tests, so you can either resolve or reject the promise based on what you want to test.
Full setup:
var $rootScope,
scope,
createController,
$q,
deferred;
var Service = {
get: function() {
deferred = $q.defer();
return {
$promise: deferred.promise
};
}
};
beforeEach(function() {
module('App');
inject(function(_$rootScope_, $controller, _$q_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
createController = function() {
$controller('MyController', {
$scope: scope,
Service: Service
});
};
$q = _$q_;
});
});
Controller implementation:
app.controller('MyController', function($scope, Service) {
$scope.property = false;
$scope.model = Service.get();
$scope.model.$promise.then(function(data) {
if (data) {
$scope.property = true;
}
});
});
You can then spy on the fake implementation to assert that it is called correctly.
Example with Jasmine:
spyOn(Service, 'get').and.callThrough();
You need and.callThrough() or the call will be interrupted and your fake implementation will not be used.
You now have full control by manually creating the controller, resolving the promise and triggering the digest loop and can test the different states:
it('Should work', function() {
spyOn(Service, 'get').and.callThrough();
expect(Service.get).not.toHaveBeenCalled();
createController();
expect(Service.get).toHaveBeenCalled();
expect(scope.property).toBeFalsy();
deferred.resolve('some data');
$rootScope.$digest();
expect(scope.property).toBeTruthy();
});
Demo: http://plnkr.co/edit/th2pLWdVa8AZWOyecWOF?p=preview

Related

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 can I test data set in a service callback when testing a controller in AngularJS?

Lets say I have a service which queries some data and sets it in the controller, a little similar to:
(Method on controller)
DogService.query(function(data)){
if(data.isSuccess){
$scope.IloveDogs = true;
$scope.dogLovers += 1;
}
})
It is highly simplified, but how would I in my controller test that when calling a mocked dogService, that it sets the correct data?
If for simplicity we say that the function isn't asynchronous and deals with promises, I would create and inject a mock to the controller. The mock could look like:
var DogService = {
query: function(){
return true;
}
}
This unfortunately doesn't run the code where the $scope.IloveDogs is set to true, and the dogLovers is incremented by one.
Any ideas, since I would rather not have to duplicate the code in my controller from the service to the mocked service?
This is how I would normally mock a service in a unit test.
(You didn't mention which testing framework you use, so I am going to assume Jasmine as it's the most popular one at the moment).
I just create a dumb object to act as my mock and then just Jasmine's built-in spy functionality to dictate what it returns. Note that this is syntax for Jasmine 2.0.
I use $q to create a promise, and make sure I am able to reference it from my tests so I can resolve it.
describe('Spec', function() {
var scope;
var catServiceMock;
var deferredCatCall;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a promise
deferredCatCall = $q.defer();
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue(deferredCatCall.promise);
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//resolve the promise that was returned by the mock
deferredCatCall.resolve({
isSuccess: true
});
//Need to trigger a $digest loop so angular process the resolved promise
scope.$digest();
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
For a service that does not use promises, I would possibly do something like this:
describe('Spec', function() {
var scope;
var catServiceMock;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a value
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue({
isSuccess: true
});
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
The main problem with this approach is that you're forced to dictate what the service will return before you instantiate the controller. This means that if you want to test how the controller behaves to different data received from the service you're going to have to have multiple beforeEach blocks nested in different describe blocks and while it looks at a glance like it's less boilerplate in the test you will end up with a lot more.
This is one of the reasons why I prefer my services to return promises even if they are not asynchronous.

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.

AngularJS: how do I use an angular service during module configuration time?

See this plunkr for a live example: http://plnkr.co/edit/djQPW7g4HIuxDIm4K8RC
In the code below, the line var promise = serviceThatReturnsPromise(); is run during module configuration time, but I want to mock out the promise that is returned by the service.
Ideally I'd use the $q service to create the mock promise, but I can't do that because serviceThatReturnsPromise() is executed during module configuration time, before I can get access to $q. What's the best way to resolve this chicken and egg problem?
var app = angular.module('plunker', []);
app.factory('serviceUnderTest', function (serviceThatReturnsPromise) {
// We mock out serviceThatReturnsPromise in the test
var promise = serviceThatReturnsPromise();
return function() {
return 4;
};
});
describe('Mocking a promise', function() {
var deferredForMock, service;
beforeEach(module('plunker'));
beforeEach(module(function($provide) {
$provide.factory('serviceThatReturnsPromise', function() {
return function() {
// deferredForMock will be undefined because this is called
// when `serviceUnderTest` is $invoked (i.e. at module configuration),
// but we don't define deferredForMock until the inject() below because
// we need the $q service to create it. How to solve this chicken and
// egg problem?
return deferredForMock.promise;
}
});
}));
beforeEach(inject(function($q, serviceUnderTest) {
service = serviceUnderTest;
deferredForMock = $q.defer();
}));
it('This test won\'t even run', function() {
// we won't even get here because the serviceUnderTest
// service will fail during module configuration
expect(service()).toBe(4);
});
});
I'm not sure I like the solution much, but here it is:
http://plnkr.co/edit/uBwsJxJRjS1qqsKIx5j7?p=preview
You need to ensure that you don't instantiate "serviceUnderTest" until after you've set-up everything. Therefore, I've split the second beforeEach into two separate pieces: the first instantiates and uses $q, the second instantiates and uses serviceUnderTest.
I've also had to include the $rootScope, because Angular's promises are designed to work within a $apply() method.
Hope that helps.

Resources