AngularJS unit test controller with only private functions - angularjs

I have following controller that has only private functions. I am struggling to test this controller. Shall I test if it is doing $emit, since the ImageService has been tested? How do I test $emit in this case? Or shall I test if it is calling the ImageService.fetchImageStacks method? In this case, how do I trigger init function?
(function (angular, global, undefined) {
'use strict';
var ImageController = {};
ImageController.$inject = [
'$rootScope',
'$log',
'ImageService'
];
ImageController = function (
$rootScope,
$log,
ImageService
) {
var getImageStacks = function() {
ImageService
.fetchImageStacks()
.success(function (result) {
ImageService.setImageStacks(result);
$rootScope.$emit('rootScope:imageStacksUpdated', result);
})
.error(function (){
$log.error('Failed to get imageStackInfo file.');
});
};
var init = function () {
getImageStacks();
};
init();
return {
getImageStacks: getImageStacks
};
}
angular.module('myApp')
.controller('ImageController', ImageController);
})(angular, this);

You shouldn't be testing private/internal methods that are not available to the outside world(imho).
Some resources on the subject (for & against):
http://www.peterprovost.org/blog/2012/05/31/my-take-on-unit-testing-private-methods/
http://www.quora.com/Should-you-unit-test-private-methods-on-a-class
http://henrikwarne.com/2014/02/09/unit-testing-private-methods/
Should Private/Protected methods be under unit test?
Should I test private methods or only public ones?
With that said, you are exposing getImageStacks on the controller - so it isn't a private method. If you were to log out the result of instantiating the controller in your test suite, you should see something of the sort:
{ getImageStacks: function }
(init() in your case, is just an alias for getImageStacks (aka there is no need for the init method - you could just call getImageStacks and be done with it)).
Anyway, to write some tests;
First of, you should stub out the ImageService as we are not interested in the internal implementation of said service, we are only ever interested in the communication going from the controller to the service. A great library for stubbing/mocking/spying is sinonjs - get it, you won't regret it.
In the beforeEach I would suggest you do something like this:
// Stub out the ImageService
var ImageService = {
fetchImageStacks: sinon.stub(),
setImageStacks: sinon.stub()
};
var $scope, instantiateController;
beforeEach(function () {
// Override the ImageService residing in 'your_module_name'
module('your_module_name', function ($provide) {
$provide.value('ImageService', ImageService);
});
// Setup a method for instantiating your controller on a per-spec basis.
instantiateController = inject(function ($rootScope, $controller, $injector) {
ctrl = $controller('ImageController', {
$scope: $rootScope.$new(),
// Inject the stubbed out ImageService.
ImageService: $injector.get('ImageService')
});
});
});
Now you have a stubbed out ImageService to test calls against, and a method to instantiate your controller with the dependencies passed into it.
Some example specs you can run;
it('calls the ImageService.fetchImageStacks method on init', function () {
instantiateController();
expect(ImageService.fetchImageStacks).to.have.been.calledOnce;
});
it('calls the ImageService.setImageStacks on success', inject(function ($q, $timeout) {
ImageService.getImageStacks.returns($q.when('value'));
instantiateController();
$timeout.flush();
expect(ImageService.setImageStacks).to.have.been.calledOnce.and.calledWith('value');
}));
I hope that will suffice and answer your questions on;
If/when you should/shouldn't test internal implementation.
How to test the initialisation of the controller.
How to test methods of an injected service.

Related

Angular JS Unit Testing (Karma Jasmine)

This is my service
angular.module('providers',)
.provider('sample', function(){
this.getName = function(){
return 'name';
};
this.$get = function($http, $log, $q, $localStorage, $sessionStorage) {
this.getTest = function(){
return 'test';
};
};
});
This is my unit test
describe('ProvideTest', function()
{
beforeEach(module("providers"));
beforeEach(function(){
module(function(sampleProvider){
sampleProviderObj=sampleProvider;
});
});
beforeEach(inject());
it('Should call Name', function()
{
expect(sampleProviderObj.getName()).toBe('name');
});
it('Should call test', function()
{
expect(sampleProviderObj.getTest()).toBe('test');
});
});
I am getting an error Type Error: 'undefined' is not a function evaluating sampleProviderObj.getTest()
I need a way to access function inside this.$get . Please help
You should inject your service into the test. Replace this:
beforeEach(function(){
module(function(sampleProvider){
sampleProviderObj=sampleProvider;
});
});
beforeEach(inject());
With this:
beforeEach(inject(function(_sampleProvider_) {
sampleProvider = _sampleProvider_;
}));
Firstly, you need, as had already been said, inject service, that you test. Like following
beforeEach(angular.mock.inject(function ($injector) {
sampleProviderObj = $injector.get('sample');
}));
Second, and more important thing. Sample have no any getTest functions. If you really need to test this function, you should as "Arrange" part of your test execute also $get function of your provider. And then test getTest function of result of previous execution. Like this:
it('Should call test', function()
{
var nestedObj = sampleProviderObj.$get(/*provide correct parameters for this function*/)
expect(nestedObj.getTest()).toBe('test');
});
But it's not good because this test can fail even if nestedObj.getTest work properly (in case when sampleProviderObj.$get works incorrect).
And one more thing, it seems like you need to inject this services $http, $log, $q, $localStorage, $sessionStorage to you provider rather then passing them as parameters.

How to test local functions within angular controllers?

Let's suppose I have a controller like:
angular
.module("app", [])
.controller("NewsFeedController", [
"$scope",
"NewsFeedService",
function ($scope, NewsFeedService) {
$scope.news = [
{ stamp: 1 },
{ stamp: 9 },
{ stamp: 0 }
];
$scope.onScroll = function () {
/*
might do some stuff like debouncing,
checking if there's news left to load,
check for user's role, whatever.
*/
var oldestStamp = getOldestNews().stamp;
NewsFeedService.getOlderThan(oldestStamp);
/* handle the request, append the result, ... */
};
function getOldestNews () {
/* code supposed to return the oldest news */
}
}
]);
getOldestNews is declared as a local function since there is no point to expose it in the $scope.
How should I deal with it? How can I actually test this function?
describe("NewsFeedController", function () {
beforeEach(module("app"));
var $controller, $scope, controller;
beforeEach(inject(function (_$controller_) {
$controller = _$controller_;
$scope = {};
controller = $controller("NewsFeedController", { $scope: $scope });
}));
it("should return the oldest news", function () {
// How do I test getOldestNews?
});
});
By the way, it'd be great if the solution also works for local functions within services and directives.
Related questions:
How can we test non-scope angular controller methods?
How do I mock local variable for a function in a service? Jasmine/Karma tests
Now I see what you really want to do in your code. I don't think it is necessary to test the private function, because it does not contain enough logic. I would suggest you only create a spy on the NewsFeedService to test that the correct data is send to that service.
describe("NewsFeedController", function () {
beforeEach(module("app"));
var $controller, $scope, controller;
var newsFeedServiceMock = jasmine.createSpyObj('NewsFeedService', ['getOlderThan']);
beforeEach(inject(function (_$controller_) {
$controller = _$controller_;
$scope = {};
controller = $controller("NewsFeedController", { $scope: $scope, NewsFeedService : newsFeedServiceMock });
}));
it("should return the oldest news", function () {
$scope.news = [
{ stamp: 76 },
{ stamp: 4 },
{ stamp: 83 }
];
$scope.onScroll();
expect(newsFeedServiceMock.getOlderThan).toHaveBeenCalledWith(83);
});
});
This way you can check if the correct behaviour is done by your onScroll method, without the need to check private methods. You only want to test the public methods, so you have flexibility when you want to create private methods to separate logic, without having to alter your tests.

Jasmine doesn't execute callbacks from $resource

So I'm having this issue when writing my tests that I don't know how to solve:
This is my controller:
'use strict';
angular.module('testApp')
.controller('SettingsExtrasCtrl', function ($scope, $log, Auth, Property, $modal, dialogs, growl) {
$scope.deleteExtra = function(index) {
var dlg = dialogs.confirm('Please Confirm', 'Are you sure you want to delete '+$scope.selectedProperty.extras[index].name+'?');
dlg.result.then(function() {
Property.removeExtra({ _id : $scope.selectedProperty._id, otherId : $scope.selectedProperty.extras[index]._id }, function(res) {
$scope.selectedProperty.extras.splice(index,1);
growl.success("Success message", {title : 'Success'});
},
function(err) {
console.log(err);
});
});
};
});
$scope.selectedProperty comes from a parent controller.
And here is my test:
'use strict';
describe('Controller: SettingsExtrasCtrl', function () {
// load the controller's module
beforeEach(module('testApp'));
var SettingsExtrasCtrl, scope, stateParams, Property, httpBackend;
var dialogs = {
confirm: function (title, message) {
return {
result: {
then: function (callback) {
return callback();
}
}
}
}
};
var fakeProperty = {
_id : 'propertyId',
extras : [
{
_id : 'extraId',
name : 'Extra'
}
]
};
beforeEach(inject(function ($controller, $rootScope, _Property_, _$httpBackend_, $state, $modal, _dialogs_) {
scope = $rootScope.$new();
scope.selectedProperty = fakeProperty;
stateParams = {propertyId: fakeProperty._id};
Property = _Property_;
httpBackend = _$httpBackend_;
spyOn(Property, 'removeExtra');
spyOn(_dialogs_, 'confirm').andCallFake(dialogs.confirm);
SettingsExtrasCtrl = $controller('SettingsExtrasCtrl', {
$scope: scope,
$stateParams: stateParams,
dialogs: _dialogs_,
$state: $state
});
}));
it('should delete an extra', inject(function(_dialogs_) {
httpBackend.expectDELETE('/api/properties/' + stateParams.propertyId + '/extras/someextraId').respond(200, '');
scope.deleteExtra(0);
expect(_dialogs_.confirm).toHaveBeenCalled();
expect(Property.removeExtra).toHaveBeenCalled();
expect(scope.selectedProperty.extras.length).toBe(0);
}));
});
The assert expect(scope.selectedProperty.extras.length).toBe(0); fails because expects 1 to be 0 because the success callback from Property.removeExtra is never called.
Any idea on how to solve this?
Thanks.
For promise to be executed you have to call a digest cycle :
scope.deleteExtra(0);
scope.$digest();
[EDIT]
Has it's a network call, you will have to look at $httpBackend
basically it work like that :
//you can mock the network call
$httpBackend.whenGET('https://url').respond(200);//status code or object
// stuff that make the call
....
$httpBackend.flush();
expect(thing).toBe(stuff);
A bit of doc :
The $httpBackend used in production always responds to requests asynchronously. If we preserved this behavior in unit testing, we'd have to create async unit tests, which are hard to write, to follow and to maintain. But neither can the testing mock respond synchronously; that would change the execution of the code under test. For this reason, the mock $httpBackend has a flush() method, which allows the test to explicitly flush pending requests. This preserves the async api of the backend, while allowing the test to execute synchronously.

AngularJS Jasmine error 'Controller is not a function' when instantiated with arguments

I have been doing angularJS for a while now (without tests) but I want to do it properly! I have a controller defined like so
(function () {
'use strict';
angular.module('app')
.controller('CarehomeListCtrl', ['$scope', 'carehomesDataService', carehomeListCtrl]);
function carehomeListCtrl($scope, carehomesDataService) {
var vm = this;
vm.carehomeCollection = [];
vm.activate = activate;
function activate() {
vm.carehomeCollection = carehomesDataService.getAllCarehomes();
}
activate();
}
})();
and then my spec
describe("Carehomes tests", function () {
var $scopeConstructor, $controllerConstructor;
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope) {
$controllerConstructor = $controller;
$scopeConstructor = $rootScope;
}));
describe("CarehomeListCtrl", function () {
var ctrl, dataService, scope;
function createController() {
return $controllerConstructor('CarehomeListCtrl', {
$scope: scope,
carehomesDataService: dataService
});
}
beforeEach(inject(function ($injector) {
scope = $scopeConstructor.$new();
dataService =$injector.get('carehomesDataService') ;
}));
it("should have a carehomesCollection array", function () {
ctrl = createController();
expect(ctrl.carehomesCollection).not.toBeNull();
});
it("should have 3 items in carehomesCollection array when load is called", function () {
ctrl = createController();
expect(ctrl.carehomeCollection.length).toBe(3);
});
});
});
The problem here is that the call to instantiate my controller fails with error whenever I call it with any arguments whether an empty object {} or just $scope : scope} so I know the problem is not carehomesDataService.
Result StackTrace: Error: [ng:areq] Argument 'CarehomeListCtrl' is not
a function, got undefined
http://errors.angularjs.org/1.2.26/ng/areq?p0=CarehomeListCtrl&p1=not%20a%20function%2C%20got%20undefined
However, if I instantiate that controller like this $controllerConstructor('CarehomeListCtrl'); without arguments, it gets instantiated. I'm stumped!
carehomesDataService is a custom service I have written but it's own tests pass and it is correctly injected into the controller in the application.
Any help would be massively appreciated.
Note: I do not quite agree with defining properties on the controller as the view model instead of on $scope but I am following Jesse Liberty's pluralsight course and that's how he does it....plus injecting scope isn't quite working right now which is annoying. Thanks in advance.

Unit test AngularJS controller that inherits from a base controller via $controller

The scenario is I have a ChildCtrl controller that inherits from BaseCtrl following this inheritance pattern:
angular.module('my-module', [])
.controller('BaseCtrl', function ($scope, frobnicate) {
console.log('BaseCtrl instantiated');
$scope.foo = frobnicate();
// do a bunch of stuff
})
.controller('ChildCtrl', function ($controller, $scope) {
$controller('BaseCtrl', {
$scope: $scope,
frobnicate: function () {
return 123;
}
});
});
Assuming BaseCtrl does a bunch of stuff and is already well tested, I want to test that ChildCtrl instantiates BaseCtrl with certain arguments. My initial thought was something along these lines:
describe("ChildCtrl", function () {
var BaseCtrl;
beforeEach(module('my-module'));
beforeEach(module(function($provide) {
BaseCtrl = jasmine.createSpy();
$provide.value('BaseCtrl', BaseCtrl);
}));
it("inherits from BaseCtrl", inject(function ($controller, $rootScope) {
$controller('ChildCtrl', { $scope: $rootScope.$new() });
expect(BaseCtrl).toHaveBeenCalled();
}));
});
However when I run the test the spy is never called and the console shows "BaseCtrl instantiated", indicating that $controller is using the actual controller instead of the instance I am providing with $provide.value().
What's the best way to test this?
So it looks like $controller doesn't search for controllers by name in the $provide.value() namespace. Instead you have to use the $controllerProvider.register() method, which is only accessible from the module.config() block. Fortunately it looks like there's a hook we can use to get access to $controllerProvider on the module under test.
The updated test code looks like:
describe("ChildCtrl", function () {
var BaseCtrl;
beforeEach(module('my-module', function ($controllerProvider) {
BaseCtrl = jasmine.createSpy();
BaseCtrl.$inject = ['$scope', 'frobnicate'];
$controllerProvider.register('BaseCtrl', BaseCtrl);
}));
beforeEach(inject(function ($controller, $rootScope) {
$controller('ChildCtrl', { $scope: $rootScope.$new() });
}));
it("inherits from BaseCtrl", inject(function ($controller, $rootScope) {
expect(BaseCtrl).toHaveBeenCalled();
}));
it("passes frobnicate() function to BaseCtrl that returns 123", function () {
var args = BaseCtrl.calls.argsFor(0);
var frobnicate = args[1];
expect(frobnicate()).toEqual(123);
});
});

Resources