Angular Unit Testing Controllers - javascript

My controller doesnt do a lot other than call methods in a service, the service wraps up and returns its functions, I have already written unit tests for the service mocking the http request.
Is it even worth unit testing the controller in this instance and if so what would I be testing as I have already tested the service functionality.
Below is my controller:
'use strict';
/* Controllers */
var calculatorControllers = angular.module('calculatorControllers', []);
calculatorControllers.controller('BodyController', ['$scope',
function($scope) {
$scope.toggleNavBarActive = function($event) {
$($event.currentTarget).parent().find('.active').removeClass('active');
$($event.currentTarget).addClass('active');
};
}]);
calculatorControllers.controller('CalculatorCtrl', ['$scope', 'CalculatorService',
function($scope, CalculatorService) {
$scope.orderProp = 'asc';
$scope.result = ' awaiting calculation';
$scope.sum = {};
$scope.add = function(val1, val2) {
var promise = CalculatorService.add(val1, val2);
promise.then(function(response) {
$scope.result = response;
});
};
}]);
calculatorControllers.controller('AboutCtrl', ['$scope', '$routeParams',
function($scope, $routeParams) {
}]);

Is it even worth unit testing the controller in this instance
Yes, you should aim for 100% coverage, not matter controller or service. I would test two things here (Jasmine):
it('inits $scope', function() {
var $scope = {};
$controller('PasswordController', { $scope: $scope });
expect($scope.orderProp).toEqual('asc');
expect($scope.result).toEqual(' awaiting calculation');
expect($scope.sum).toEqual({});
});
it('calls CalculatorService and sets the result', function() {
var $scope = {};
$controller('PasswordController', { $scope: $scope });
$scope.sum(1, 2);
expect(CalculatorServiceMock).toHaveBeenCalledWith(1, 2);
resolveCalculatorServiceMockAddSpyWith(3);
expect($scope.result).toEqual(3);
});

The only case when the controller methods don't require testing is
$scope.calculator = CalculatorService;
So all view calls like {{ calculator.sum(...) }} are done by the service.
In every other case controller methods should be tested. Since CalculatorService unit was already tested, it has to be mocked in order for controller logic to be tested in isolation.

Related

testing angularjs 1 factory method is automatically called inside a controller with jasmine

I'm using ruby on rails with angularjs one, and testing it with teaspoon-jasmine for the first time and am running into issues. Basically, I have a controller that creates an empty array and upon load calls a factory method to populate that array. The Factory makes an http request and returns the data. Right now, i'm trying to test the controller, and i'm trying to test that 1) the factory method is called upon loading the controller, and 2) that the controller correctly assigns the returned data through it's callback. For a while I was having trouble getting a mocked factory to pass a test, but once I did, I realized I wasn't actually testing my controller anymore, but the code below passes. Any tips on how I can still get it to pass with mock, promises/callbacks, but accurately test my controller functionality. Or should I even test the this at all in my controller since it calls a factory method and just gives it a callback? My 3 files are below. Can anyone help here? It would be greatly appreciated
mainController.js
'use strict';
myApp.controller('mainController', [ 'mainFactory', '$scope', '$resource', function(factory, scope, resource){
//hits the /games server route upon page load via the factory to grab the list of video games
scope.games = [];
factory.populateTable(function(data){
scope.games = data;
});
}]);
mainFactory.js
'use strict';
myApp.factory('mainFactory', ['$http', '$routeParams', '$location', function(http, routeParams, location) {
var factory = {};
factory.populateTable = function(callback) {
http.get('/games')
.then(function(response){
callback(response.data);
})
};
return factory;
}]);
And finally my mainController_spec.js file
'use strict';
describe("mainController", function() {
var scope,
ctrl,
deferred,
mainFactoryMock;
var gamesArray = [
{name: 'Mario World', manufacturer: 'Nintendo'},
{name: 'Sonic', manufacturer: 'Sega'}
];
var ngInject = angular.mock.inject;
var ngModule = angular.mock.module;
var setupController = function() {
ngInject( function($rootScope, $controller, $q) {
deferred = $q.defer();
deferred.resolve(gamesArray);
mainFactoryMock = {
populateTable: function() {}
};
spyOn(mainFactoryMock, 'populateTable').and.returnValue(deferred.promise);
scope = $rootScope.$new();
ctrl = $controller('mainController', {
mainFactory: mainFactoryMock,
$scope: scope
});
})
}
beforeEach(ngModule("angularApp"));
beforeEach(function(){
setupController();
});
it('should start with an empty games array and populate the array upon load via a factory method', function(){
expect(scope.games).toEqual([])
mainFactoryMock.populateTable();
expect(mainFactoryMock.populateTable).toHaveBeenCalled();
mainFactoryMock.populateTable().then(function(d) {
scope.games = d;
});
scope.$apply(); // resolve promise
expect(scope.games).toEqual(gamesArray)
})
});
Your code looks "non-standard" e.g still using scope.
If you are just starting with angular I hardly recommend you to read and follow this:
https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
Angular controllers cannot be tested, extract the logic into factories/services and test from there.

How to test angular app with John Papa style, Karma, and Jasmine with data service?

For some reason, even following the example provided by Josh in the post reply How to test John papa vm.model unit testing with jasmine?, I can't get my controller's values to show up in the testing area. I think it's because of the data service, but it is a necessary component for our SPA, as is using John Papa's styling.
Below is a code snippet to hold the code and display the errors I'm receiving.
(function() {
'use strict';
angular.module('tickets')
.service("DataService", DataService)
/* #ngInject */
DataService.$inject = ["$rootScope", "$q"];
function DataService($rootScope, $q) {
var vm = this;
vm.nameDefault = "Name -- Please Authenticate";
vm.name = vm.nameDefault;
};
})();
(function() {
'use strict';
angular.module('tickets')
.controller('HomeController', HomeController);
/* #ngInject */
HomeController.$inject = ['$scope', '$location', 'DataService'];
function HomeController($scope, $location, DataService) {
var vm = this;
vm.name = DataService.name;
vm.nameDefault = DataService.nameDefault;
};
})();
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular-mocks.js"></script>
<script>
'use strict';
describe('Controller: HomeController', function() {
beforeEach(module('tickets'));
var controller, $location, DataService;
var tests = 0;
beforeEach(inject(function($controller, _$location_, _DataService_) {
$location = _$location_;
DataService = _DataService_;
scope = {};
controller = $controller('HomeController', {});
}));
var controller, scope, $location, DataService;
var tests = 0;
/* // This version works up until I try to verify the name and nameDefault in controller \\
*
* beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
* $location = _$location_;
* DataService = _DataService_;
* scope = $rootScope.$new();
*
* controller = function () {
* return $controller('HomeController', {});
* };
* }));
*/
afterEach(function() {
tests += 1;
});
describe('local variables', function() {
describe('load the data model and values for all pertinent variables', function() {
it('should be instantiated', function() {
expect(DataService).toBeDefined();
});
it('should contain a name with an initial value before authentication', function() {
expect(DataService.nameDefault).toBe('Name -- Please Authenticate');
expect(DataService.name).toEqual(DataService.nameDefault);
});
});
describe('should load the controller with data model values, and update as data model values update', function() {
it('should be instantiated', function() {
expect(controller).toBeDefined();
})
it('should not have a vm attribute that can be reached from here', function() {
expect(controller.vm).toBeUndefined();
})
it('should contain a name with an initial value before authentication, both from the data model', function() {
expect(controller.name).toBe(DataService.name);
expect(controller.nameDefault).toBe(DataService.nameDefault);
});
});
});
it('should have tests', function() {
expect(tests).toBeGreaterThan(0);
});
});
</script>
My code, when I use it in our native environment, works to verify that everything in the data service has been instantiated properly (and using the commented out beforeEach block), but the styling using the example in the question referenced above throws even more errors about the scope not being instantiated, even though it is the same (with added dependencies) as that question.
I would expect the answer to be similar to the (currently unanswered) question: How to test John papa vm.model controllers and factories unit testing with jasmine?
I appreciate any help you guys offer.
-C§
Edit Even though I've answered and have success, I would love to get some feedback on why the implementation below works and the other two attempts do not. This is using Karma version 0.13.8 (latest), jasmine 2.1.0, and Angular 1.4.0.
I know it seems like I came up with the solution pretty quickly, but I've been wrestling with this since Friday afternoon (8/7) and have tried a dozen different formats without success.
Again, I welcome your comments and votes so that I can better understand why the version below works and the others have not, especially since I am still very green to AngularJS (1 month in, now).
Thanks again,
-C§
I get it now. Just had to look at Globally mock object in angularjs for jasmine/karma testing and my brain clicked.
The declaration and beforeEach block in the beginning of the test needs to look like this:
var controller, scope, $location, DataService;
var tests = 0;
beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
$location = _$location_;
DataService = _DataService_;
scope = $rootScope.$new();
controller = $controller('HomeController', {
$scope: scope
});
}));
I think since I've messed with our initial setup a little too much (started the SPA from a template), I needed a strange implementation to make it all work. I'm now getting successful tests throughout:
I hope this helps someone else with similar issues.

What's the right way to test this controller?

I'm writing a test for a controller, using angular.js + jasmine.
Let's say I have a controller:
angular.module('app').controller('HomePageCtrl', function($scope, Homepage, posts, topics, photos, sideBar) {
$scope.slide = (photos || [])[0];
$scope.posts = Homepage.posts(posts);
$scope.topics = Homepage.topics(topics);
$scope.sideBar = sideBar;
$scope.template = '/templates/home-page.html';
});
This controller has only scope variables assignments not functions. How can I write tests for this controller? I need to check if each variables is defined? Each variable has the right value?
You could use the log service (if that is what you mean) to test if your variables are not empty
angular.module('logExample', [])
.controller('LogController', ['$scope', '$log', function($scope, $log) {
$scope.$log = $log;
$scope.message = 'Hello World!';
}]);
You can put all initialization logic into an exposed function, to be able to call it from tests.
angular.module('app').controller('HomePageCtrl', function($scope, Homepage, posts, topics, photos, sideBar) {
$scope.activate = function () {
$scope.slide = (photos || [])[0];
$scope.posts = Homepage.posts(posts);
$scope.topics = Homepage.topics(topics);
$scope.sideBar = sideBar;
$scope.template = '/templates/home-page.html';
}
$scope.activate();
});

How can i add a scope to a http controller in angular.js?

I also want to have acces to a $scope in this controller so I can do methods like ng-click and ng-change in my html. I know have this $http but i cant do anny angular function on it and I can't seem to figure out how I can use a $scope in this controller.
app.controller('songController', ['$http', function($http) {
$songs = this;
$songs.tracks = [];
$http({
url: "http://api.q-music.be/1.2/tracks/plays?limit=20",
dataType: "jsonp",
jsonp: "callback"
})
.success(function(lastSongsList) {
$songs.tracks = lastSongsList.played_tracks;
console.log($songs.tracks);
});
}]);
I want to be able to do methods in this controller like this :
$scope.change = function() {
$scope.counter++;
};
You need to inject your scope dependency like this :
app.controller('songController', ['$http', '$scope', function($http, $scope) {
Change your controller definition to this:
app.controller('songController', ['$http', '$scope', function($http, $scope) {
The same way that you inject $http into your controller, you need to do the same with $scope to allow your controller to use it.
Look up dependency injection and you'll find all kinds of stuff about. It's pretty useful and you'll want to understand how it works if you're going to be working with Angular.
$songs = this;
$songs.tracks = [];
should also be
$scope.songs = this;
$scope.songs.tracks = [];
same for the $http success handler
$songs.tracks = lastSongsList.played_tracks;
should be
$scope.songs.tracks = lastSongsList.played_tracks;
Your change function can remain in the controller
$scope.change = function() {
$scope.counter++;
};
Then in your html, e.g. button input use ng-click="change()"

AngularJS: Unit Testing Directive w/ Promise returned from a Service

I have a directive which uses a Service, calls a method of the service which returns a promise, and does work modifying the DOM inside the subsquent 'then' (myTestDirective below).
I'm trying to unit test this directive and when I run the test nothing inside of the 'then' is being called: the promise is rejected or the resolution not propagated?
I followed all the steps in this post to setup my unit test
When I load the directive in the browser I see both messages, OUTSIDE D3 then INSIDE D3, as you'd expect.
However in the unit test the element is updated only with the first message, like so:
<my-test-directive>***OUTSIDE D3***</my-test-directive> .
In the browser I see both messages.
Does anybody know what is going on here, do I need to inject mock or spyOn something? Is this an async problem where the test runs before script tag finished loading? I see the unit test accessing d3.v3.js, so it appears the script tag happens. I have also unit tested the d3Service on it's own, and it worked. Once in a while I actually see the correct results without changing the test at all.
I see clues in this question but unable to understand how to apply it in my situation: Angularjs promise not being resolved in unit test
Here is the code:
d3Service:
var d3 = angular.module('d3', []);
d3.factory('d3Service', ['$document', '$q', '$rootScope', '$window',
function($document, $q, $rootScope, $window) {
var d = $q.defer();
function onScriptLoad() {
$rootScope.$apply(function() { d.resolve(window.d3); });
}
var scriptTag = $document[0].createElement('script');
scriptTag.type = 'text/javascript';
scriptTag.async = true;
scriptTag.src = 'lib/d3.v3.js';
scriptTag.onreadystatechange = function () {
if (this.readyState == 'complete') onScriptLoad();
}
scriptTag.onload = onScriptLoad;
var s = $document[0].getElementsByTagName('body')[0];
s.appendChild(scriptTag);
return {
d3: function() { return d.promise; }
};
}]);
Directive
var myDirectives = angular.module('myDirectives', ['d3']);
myDirectives.directive('myTestDirective', ['d3Service', '$window',
function(d3Service, $window) {
return {
restrict: 'EA',
link: function(scope, ele, attrs) {
var f = angular.element(ele[0])
f.append('**OUTSIDE D3***')
d3Service.d3().then(function(d3){ // Nothing here runs at all.
var e = angular.element(ele[0]) // In browser it works, but
e.append('***INSIDE D3***') // not in the unit test.
})
}
}
}])
Unit Test
describe('Test Directive', function(){
var $scope, elem, compiled, html;
beforeEach(function (){
module('myDirectives');
html = '<my-test-directive></my-test-directive>';
inject(function($compile, $rootScope) {
$scope = $rootScope;
elem = angular.element(html);
compiled = $compile(elem)($scope);
$scope.$digest();
});
});
it('should create an svg element with its data', function(){
console.log(elem) //outputs the element with only ***OUTSIDE D3***
})
})
Thanks for any tips or information!!!!!
What I did was load d3.v3.js in my karma.conf and then create mockd3Service in unit test that return a promise. If anybody know better solution let me know.
Here is new unit test that is working:
describe('d3 Directives', function(){
var $compile, $rootScope, $window, mockd3Service, $q, html, element, data;
//setup test
beforeEach(function(){
mockd3Service = {}
module('myDirectives')
module(function($provide){
$provide.value('d3Service', mockd3Service)
})
inject(function(_$compile_, _$rootScope_, _$window_, _$q_) {
$window = _$window_;
$compile = _$compile_;
$rootScope = _$rootScope_;
$q = _$q_
});
mockd3Service.d3 = function() {
var deferred = $q.defer();
deferred.resolve($window.d3)
return deferred.promise;
}
});
//run test
it('make test', function(){
html = '<my-directive data="testData"></my-directive>'
element = angular.element(html)
element = $compile(html)($rootScope)
$rootScope.testData = somedata
$rootScope.$digest();
expect(element...)
})
})

Categories

Resources