Why is there a difference between these approaches in testing a controller - angularjs

While doing a Jasmine test for an Angular Controller, I find a difference between these two approaches. There shouldn't be, but there is. That said, using debug, I find in both cases the correct mocked items are coming thru, however the tests behave differently.
First: Here we mock service items which are then injected using DI into the controller at creation.
$provide.value('core.data.CompanyService', companyService);
$provide.value('core.list.ListGenerator', listGeneratorFactory);
$provide.value('core.actions.ActionContext', actionContext);
ActivitiesCtrl = $controller('activities.ActivitiesCtrl', {
$scope: scope
});
Second:
Here we explicitly specify the injected service items in the controller creation:
ActivitiesCtrl = $controller('activities.ActivitiesCtrl', {
$scope: scope,
'core.lists.ListGenerator': listGeneratorFactory,
'core.actions.ActionContext': actionContext,
'core.data.CompanyService': companyService
});

If your first snippet of code is actually what you have, then I think I see the problem; providers should be set in a module config section and $controller should be accessed within an inject callback.
Given proper setup of the mocks prior to this, the following are equivalent
Providers on the $injector
beforeEach(function() {
module('your.controller.module', function($provide) {
$provide.value('core.list.ListGenerator', listGeneratorFactory);
$provide.value('core.actions.ActionContext', actionContext);
$provide.value('core.data.CompanyService', companyService);
});
inject(function($controller) {
// assuming scope is defined somewhere
ActivitiesCtrl = $controller('activities.ActivitiesCtrl', {
$scope: scope
});
});
});
Controller locals
beforeEach(inject($controller) {
// again, assuming scope is defined somewhere
ActivitiesCtrl = $controller('activities.ActivitiesCtrl', {
$scope: scope,
'core.lists.ListGenerator': listGeneratorFactory,
'core.actions.ActionContext': actionContext,
'core.data.CompanyService': companyService
});
}));

Related

Testing angular controllers that have many dependencies

I am trying to test a controller.
someModule.controller('MyController', function($rootScope, $scope, dep1, dep2) {
...
$scope.aMethod = function() {
...
}
function bMethod() {
...
}
...
}]);
And I define my test this way:
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
console.log($controller);
MyController = $controller('MyController', {
$rootScope : $rootScope,
$scope: scope
});
console.log('Some debug message');
console.log(MyController);
}));
And I get this output:
LOG: function (expression, locals, later, ident) { ... }
LOG: 'Some debug message'
LOG: {}
Even if I put in dep1 and dep2 I get the same results. So scope and rootScope have to be present otherwise I get an error when it runs.
I am not certain why this isn't working, as the last output is empty, so there are no functions in the controller, which is wrong.
I want to test bMethod as a minimum, but it appears to not be creating my controller correctly.
The bMethod it's not attached to scope and neither to thisvariable, therefore it's not available in the tests. There are two ways to add the method in the test:
$scope.bMethod = bMethod; //Attached to scope
this.bMethod = bMethod; //Attached to controller
In your describe block, you prints MyController. This it's a instance of controller. If you want get the a method provided in the example, prints scope and you'll see the a method.
check this codepen --> http://codepen.io/gpincheiraa/pen/WwXGxV
There are no properties on controller instance, because they weren't defined.
It is scope object that has got aMethod property, not MyController:
expect(scope.aMethod).toBe(jasmine.any(Function));

Unit testing controllers in angularJS

I am having a hard time understanding unit tests in angularJs. I have just started with unit tests and the syntax seems weird to me. Below is the code for testing a controller :
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
beforeEach(module('phonecatApp'));
it('should create "phones" model with 3 phones',
inject(function($controller) {
var scope = {},
ctrl = $controller('PhoneListCtrl', {$scope:scope});
expect(scope.phones.length).toBe(3);
}));
});
});
What I can understand from this syntax is that before each it block phonecatApp is initialised and that $controller service is used to get an instance of PhoneListCtrl controller.
However I am not able to understand the scope thing here. Can someone elaborate on whats behind getting the scope of the controller on this line.
ctrl = $controller('PhoneListCtrl', {$scope:scope});
Normally, at runtime, angular creates a scope and injects it into the controller function to instantiate it.
In your unit test, you instead want to create the scope by yourself and pass it to the controller function, in order to be able to see if it indeed has 3 phones after construction (for example).
You might also want to inject mock services instead of the real ones into your controller. That's what the array of objects allows in
$controller('PhoneListCtrl', {$scope:scope});
It tells angular: create an instance of the controller named 'PhoneListCtrl', but instead of creating and injecting a scope, use the one I give you.
If your controller depended on a service 'phoneService', and you wanted to inject a mock phoneService, you could do
var mockPhoneService = ...;
$controller('PhoneListCtrl', {
$scope: scope,
phoneService: mockPhoneService
});
It's not necessary to inject the scope, you can directly use the instance of controller to call the controller's functions and objects.In your example you can use like below, this will give the same result set as yours
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
beforeEach(module('phonecatApp'));
it('should create "phones" model with 3 phones',
inject(function($controller) {
var ctrl = $controller('PhoneListCtrl');
expect(ctrl.phones.length).toBe(3);
}));
});
});
and for you information each time the controller is instantiated it is bound to a $scope variable which is derived from $rootScope (i.e: child of rootscope). So you need to pass the $scope to grab the instance of controller and I am doing same thing in above example.

Spy on scope function that executes when an angular controller is initialized

I want to test that the following function is in fact called upon the initialization of this controller using jasmine. It seems like using a spy is the way to go, It just isn't working as I'd expect when I put the expectation for it to have been called in an 'it' block. I'm wondering if there is a special way to check if something was called when it wasn't called within a scope function, but just in the controller itself.
App.controller('aCtrl', [ '$scope', function($scope){
$scope.loadResponses = function(){
//do something
}
$scope.loadResponses();
}]);
//spec file
describe('test spec', function(){
beforeEach(
//rootscope assigned to scope, scope injected into controller, controller instantiation.. the expected stuff
spyOn(scope, 'loadResponses');
);
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function(){
expect(scope.loadResponses).toHaveBeenCalled();
});
});
You need to initialise the controller yourself with the scope you've created. The problem is, that you need to restructure your code. You can't spy on a non-existing function, but you need to spyOn before the function gets called.
$scope.loadResponses = function(){
//do something
}
// <-- You would need your spy attached here
$scope.loadResponses();
Since you cannot do that, you need to make the $scope.loadResponses() call elsewhere.
The code that would successfully spy on a scoped function is this:
var scope;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', {$scope: scope});
scope.$digest();
}));
it("should have been called", function() {
spyOn(scope, "loadResponses");
scope.doTheStuffThatMakedLoadResponsesCalled();
expect(scope.loadResponses).toHaveBeenCalled();
});
Setting the spy before controller instantiation (in the beforeEach) is the way to test controller functions that execute upon instantiation.
EDIT: There is more to it. As a comment points out, the function doesn't exist at the time of ctrl instantiation. To spy on that call you need to assign an arbitrary function to the variable (in this case you assign scope.getResponses to an empty function) in your setup block AFTER you have scope, but BEFORE you instantiate the controller. Then you need to write the spy (again in your setup block and BEFORE ctrl instantiation), and finally you can instantiate the controller and expect a call to have been made to that function. Sorry for the crappy answer initially
The only way I have found to test this type of scenarios is moving the method to be tested to a separate dependency, then inject it in the controller, and provide a fake in the tests instead.
Here is a very basic working example:
angular.module('test', [])
.factory('loadResponses', function() {
return function() {
//do something
}
})
.controller('aCtrl', ['$scope', 'loadResponses', function($scope, loadResponses) {
$scope.loadResponses = loadResponses;
$scope.loadResponses();
}]);
describe('test spec', function(){
var scope;
var loadResponsesInvoked = false;
var fakeLoadResponses = function () {
loadResponsesInvoked = true;
}
beforeEach(function () {
module('test', function($provide) {
$provide.value('loadResponses', fakeLoadResponses)
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
For real world code you will probably need extra work (for example, you may not always want to fake the loadResponses method), but you get the idea.
Also, here is a nice article that explains how to create fake dependencies that actually use Jasmine spies: Mocking Dependencies in AngularJS Tests
EDIT: Here is an alternative way, that uses $provide.delegate and does not replace the original method:
describe('test spec', function(){
var scope, loadResponses;
var loadResponsesInvoked = false;
beforeEach(function () {
var loadResponsesDecorator = function ($delegate) {
loadResponsesInvoked = true;
return $delegate;
}
module('test', function($provide) {
$provide.decorator('loadResponses', loadResponsesDecorator);
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
I didn't quite understand any of the answers above.
the method I often use - don't test it, instead test the output it makes..
you have not specified what loadResponses actually does.. but lets say it puts something on scope - so test existence of that..
BTW - I myself asked a similar question but on an isolated scope
angular - how to test directive with isolatedScope load?
if you still want to spy - on an unisolated scope, you could definitely use a technique..
for example, change your code to be
if ( !$scope.loadResponses ){
$scope.loadResponses = function(){}
}
$scope.loadResponses();
This way you will be able to define the spy before initializing the controller.
Another way, is like PSL suggested in the comments - move loadResponses to a service, spy on that and check it has been called.
However, as mentioned, this won't work on an isolated scope.. and so the method of testing the output of it is the only one I really recommend as it answers both scenarios.

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.

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