Unit testing controllers in angularJS - 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.

Related

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

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

Angular Testing DOM after update by Factory

Ok, I'm trying to test the outcome of a function that updates the DOM>
I have a directive that loads a template via url.
Then a controller calls a factory method to update the html table with data.
I have the tests showing that I can get the data that is all good.
but how can I test that the updates to the table have taken place?
I am using NodeJS with Karma and Jasmine.
I have followed tutorials on how to load in templates, and I have that working, I can load and access the templates in my test fine.
but when I run the method to update the table, the tests fail.
I'll give an scaled down example of what I'm trying to do. Note, this is just demo code, Not a working app.
Template.
<table><tr><td class="cell1"></td></tr></table>
Directive.
dataTable.directive('dataTable', function () {
return {
restrict: 'E',
templateUrl: 'path/to/template/dataTable.html'
};
});
Controller
dataTable.controller('dataTableController', ['$scope', 'dataTableFactory',
function ($scope, dataTableFactory){
$scope.updateTable = function(){
dataTableFactory.loadData();
// code to load data from dataTableFactory here! //
dataTableFactory.updateTable();
}
}])
Factory
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
"tableData": _tableData,
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
Unit Test
describe('dataTable Tests', function () {
var scope, element, $compile, mDataTableFactory, controller, tableData, doc, factory;
beforeEach(module('dataTable'));
beforeEach(module('app.templates')); // setup via ng-html2js
beforeEach(inject(function (_$rootScope_, _$compile_,_$controller_,_dataTableFactory_) {
scope = _$rootScope_.$new();
doc = _$compile_('<flood-guidance></flood-guidance>')(scope);
factory = _dataTableFactory_;
controller = _$controller_('dataTableController', {
$scope: scope,
$element: doc,
dataTableFactory: factory
});
scope.$digest();
}));
it("Template should contain the cell cell1", function(){
expect(doc.find('.cell1').contents().length).toBe(0);
expect(doc.find('.cell1').html()).toBeDefined();
});
// Passes fine, so I know the template loads ok.
it('Should show data in cell1',function(){
factory.tableData = {data: 'someData'};
scope.updateTable();
expect(doc.find('.cell1').contents().length).toBe(1);
expect(doc.find('.cell1').html()).toBe('SomeData');
});
});
});
Test Ouptut
Expected 0 to be 1. Expected '' to be 'someData'.
If I put the updateTable code in to the controller and call the update function there, the test passes, but I'd like to have this in a factory, how can I make this test pass (the app runs and works as expected, I just can't get a working test).
I understand this kind of testing is more focused on the UI and not exactly 'Unit Testing' but is it possible to do this?
So essentially updateTable cannot find the changes performed by factory.tableData. I guess the problem may be due to the way how your factory exposes the _tableData property.
Could you try to modify your factory like this:
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
getTableData: function() { return _tableData; },
setTableData: function(newVal) { _tableData = newVal; },
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
and then of course use the setter/getter accordingly. See if it works this way.
OK so I'm still not sure if I fully get your intention but here is a fiddle with my refactored example.
http://jsfiddle.net/ene4jebb/1/
First of all the factory shouldn't touch the DOM, that's the directives responsibility. Thus my rework passes the cellvalue (new scope property) to the directive, which renders it. Now when you call setTableData (which will change _tableData.data) and since in test environment call the $digest loop yourself, the directive will automatically redraw the new stuff.
Controller is kept thin as possible thus only providing a scope property to the factory.
As said not sure if you were after this, but hope it helps. If there are any questions just ask.

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.

Unit-testing two controllers with Jasmine in AngularJS

I'm writing my first Unit Tests with Jasmine in AngularJS. I already have some controllers, services etc. and am trying to verify them (I know: First testing, then writing the code, but as I said, I am new to unit tests and want to develop my app further with unit tests).
In my case, I have the view with two controllers like
<div ng-controller="firstCtrl">
<div ng-controller="secondCtrl"></div>
</div>
On every page-load I check the routeParameters and get data from a WebAPI (this is done by the first controller). My second controller is based on the data, which is set by the first Ctrl to the $scope.
My question is, how do I write my unit-test for the secondCtrl in an elegant way? I have also a third, fourth [...] Controller, which depend on the data which was set by the first Controller. I don't want to have duplicated code, if not necessary.
The second Controller:
angular.module("myApp.controllers.secondController", ["myApp.Model"])
.controller("secondController", function ($scope, Model) {
$scope.data.Model = Model;
$scope.doSomething = function () {
// tries to grab something from the scope, which would normally be set by the first Controller, e.g.
return $scope.firstCtrlData.x;
};
});
The spec-file:
describe('My App', function () {
beforeEach(angular.mock.module('myApp'));
describe("Controller", function () {
var controller, scope;
beforeEach(inject(function ($rootScope, $controller, _Model_) {
scope = $rootScope.$new();
controller = $controller("secondCtrl", {
scope: $scope,
Model: _Model_
});
}));
it('ensures that Model is in scope.data defined', function () {
expect($scope.data.Model).toBeDefined();
});
it('doSomething() does something', function () {
var x = $scope.doeSomething();
expect(x).toBeDefined();
// this will fail
});
});
});
It is not just one 'x' - Value, it will be a whole data structure given in json Code, so it probably isn't the best way just by putting all my data in a mocked scope object...
As I am also new to angular, it is probably not even the best approach for the whole app having two controllers, but for me it seemed to be the shortest and most effective variant.

How to mock ng-grid when unit testing a controller

I'm currently trying to write tests for existing blocks of code and running into an issue with a controller that has a nested ng-grid inside of it. The issue comes from the controller trying to interact with the grid on initialization.
Testing Software
node#0.10.14
karma#0.10.2
karma-jasmine#0.1.5
karma-chrome-launcher#0.1.2
My Test:
define(["angularjs", "angular-mocks", "jquery",
"js/3.0/report.app",
"js/3.0/report.controller",
"js/3.0/report.columns"
],
function(angular, ngMocks, jquery, oARModule, oARCtrl, ARColumns) {
"use strict";
describe("Report Center Unit Tests", function() {
var oModule;
beforeEach(function() {
oModule = angular.module("advertiser_report");
module("advertiser_report");
});
it("Advertiser Report Module should be registered", function() {
expect(oModule).not.toBeNull();
});
describe("Advertiser Report Controller", function() {
var oCtrl, scope;
beforeEach(inject(function($rootScope, $controller, $compile) {
var el = document.createElement('div');
el.setAttribute('ng-grid','gridOptions');
el.className = 'gridStyle';
scope = $rootScope.$new();
$compile(el)(scope);
oCtrl = $controller('ARController', {
$scope: scope
});
}));
it("Advertiser Report controller should be registered", function() {
expect(oCtrl).not.toBeNull();
});
});
});
});
You'll see where I've tried to create and compile an element with the ng-grid attribute. Without doing this I get the following error:
TypeError: Cannot read property 'columns' of undefined
Which is a result of the controller attempting to call things like
$scope.gridOptions.$gridScope.columns.each
So I added the creation of a div with ng-grid attribute, and got a new error:
TypeError: Cannot set property 'gridDim' of undefined
So, I tried to add scope.gridOptions before the $controller call, but this brought me back to the original error. I've been searching for way to make this work without rewriting the controller and/or templates, since they are currently working correctly in production.
Your (major!) problem here is that the controller is making assumptions about a View. It should not know about and thus not interact with ng-grid. Controllers should be View-independent! That quality (and Dependency Injection) is what makes controllers highly testable. The controller should only change the ViewModel (i.e. its $scope), and in testing you validate that the ViewModel is correct.
Doing otherwise goes against the MVVM paradigm and best practices.
If you feel like you must access the View (i.e. directives, DOM elements, etc...) from the controller, you are likely doing something wrong.
The problem in the second Failing test is gridOptions and myData is not defined prior to the compilation. Notice the sequence of the 2 statements.
Passing
oCtrl = $controller('MainCtrl', { $scope: $scope });
$compile(elm)($scope);
Failing
$compile(elm)($scope);
oCtrl = $controller('MainCtrl', { $scope: $scope });
In both cases you are trying to use the same html
elm = angular.element('<div ng-grid="gridOptions" style="width: 1000px; height: 1000px"></div>');
I suggest you get rid of
oCtrl = $controller('MainCtrl', { $scope: $scope });
maneuvers and use the following HTML element instead
elm = angular.element('<div ng-controller="MainCtrl"
ng-grid="gridOptions" style="width: 1000px; height: 1000px"></div>');
Notice ng-controller="MainCtrl".
So the end story is that you need gridOptions defined somewhere so
that it ngGrid can access it. And make sure gridOptions dependent
code in controller is deferred in a $timeout.
Also take a look at the slight changes in app.js
$timeout(function(){
//your gridOptions dependent code
$scope.gridOptions.$gridScope.columns.each(function(){
return;
});
});
Here is the working plnkr.

Resources