How to test local functions within angular controllers? - angularjs

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.

Related

AngularJS unit test controller with only private functions

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.

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.

Angular controller test on scope.variable is undefined

I'm trying to learn angular unit test with $resource.
Here I have a simple controller :
.controller('DictionaryCtrl', function ($scope, DictionaryService) {
$scope.jSearchDictionary = function () {
$scope.word = DictionaryService.getByJword({ jp: $scope.jword });
}
$scope.eSearchDictionary = function () {
$scope.word = DictionaryService.getByEword({ eng: $scope.eword });
}
})
In my view, I have 2 ng-submit (jSearchDictionary and eSearchDictionary) and i bind the corresponding word that is searched ( jword or eword ).
The service is also quite simple :
.factory('DictionaryService', function ($resource) {
return $resource('http://127.0.0.1:3000/api/nlp/words', {}, {
getByJword: { method: 'GET', params: { jp: '#jword' } },
getByEword: { method: 'GET', params: { en: '#eword' } },
})
})
Finally, here is my test.
describe('Controller: nlpCtrl', function () {
beforeEach(function () {
this.addMatchers({
toEqualData: function (expected) {
return angular.equals(this.actual, expected);
}
});
});
beforeEach(module('gakusei'));
describe('nlpCtrl', function () {
var scope,
$controller,
$httpBackend,
$stateParams,
Eword,
mockWord = [
{
"words": [
{
"readings": [
"ホッケー"
]
}
],
"count": 1
}];
beforeEach(inject(function (_$httpBackend_, $rootScope, _$controller_) {
scope = $rootScope.$new();
$controller = _$controller_;
$httpBackend = _$httpBackend_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should get a word', inject(function (DictionaryService) {
Eword = "englishWord";
$httpBackend.expectGET('http://127.0.0.1:3000/api/nlp/words?eng=englishWord')
.respond(mockWord[0]);
var ctrl = $controller('DictionaryCtrl', { $scope: scope });
var request = DictionaryService.getByEword({ eng: Eword })
$httpBackend.flush();
expect(scope.word).toEqualData(mockWord[0]);
expect(BasketService.getByJword).toBeTruthy();
expect(BasketService.getByEword).toBeTruthy();
}));
});
});
The problem is at the line :
expect(scope.word).toEqualData(mockWord[0]);
scope.word being undefined. Unit Testing is way over my head right now, I'm not sure of what I'm doing at all. If you have a solution to this particular problem, have any advices at all concerning all the code or are willing to message me and guide me a little, that would be awesome.
You have couple issues in your expectation and set up.
1) You are testing a controller and its scope, so do actions on the controller methods and set values on controller scope.
2) Instead of doing Eword = "englishWord"; you should set the value on the controller scope scope.eword = "englishWord";
3) Instead of calling service method directly DictionaryService.getByEword({ eng: Eword }) , you need to invoke the method on the scope, i.e scope.eSearchDictionary(); so that when the method is resolved it resolves with respective data and sets it on the scope.
4) Note that when you test against scope.word directly you may not get desired result since the result object will have additional properties like $promise on it. Since you are directly assigning the results.
5) I am not sure if you need the last 2 expectations at all.
Try:-
it('should get a word', inject(function (DictionaryService) {
scope.eword = "englishWord";
$httpBackend.expectGET('http://127.0.0.1:3000/api/nlp/words?eng=englishWord')
.respond(mockWord[0]);
$controller('DictionaryCtrl', { $scope: scope });
scope.eSearchDictionary();
$httpBackend.flush();
expect(scope.word.words[0]).toEqual(mockWord[0].words[0]);
/*I dont think you need the following expectations at all*/
expect(DictionaryService.getByJword).toBeDefined();
expect(DictionaryService.getByEword).toBeDefined();
}));
Plnkr
Some syntax of expectation utility method is different from what you are using, you can use the same that you use, i just did it for the demo
The variable you are looking for does not exist outside of those two functions. Try defining it at the top of your controller like so:
.controller('DictionaryCtrl', function ($scope, DictionaryService) {
$scope.word = '';
$scope.jSearchDictionary = function () {
$scope.word = DictionaryService.getByJword({ jp: $scope.jword });
}
$scope.eSearchDictionary = function () {
$scope.word = DictionaryService.getByEword({ eng: $scope.eword });
}
})

Mock a service in order to test a controller

I have a ParseService, that I would like to mock in order test all the controllers that are using it, I have been reading about jasmine spies but it is still unclear for me. Could anybody give me an example of how to mock a custom service and use it in the Controller test?
Right now I have a Controller that uses a Service to insert a book:
BookCrossingApp.controller('AddBookCtrl', function ($scope, DataService, $location) {
$scope.registerNewBook = function (book) {
DataService.registerBook(book, function (isResult, result) {
$scope.$apply(function () {
$scope.registerResult = isResult ? "Success" : result;
});
if (isResult) {
//$scope.registerResult = "Success";
$location.path('/main');
}
else {
$scope.registerResult = "Fail!";
//$location.path('/');
}
});
};
});
The service is like this:
angular.module('DataServices', [])
/**
* Parse Service
* Use Parse.com as a back-end for the application.
*/
.factory('ParseService', function () {
var ParseService = {
name: "Parse",
registerBook: function registerBook(bookk, callback) {
var book = new Book();
book.set("title", bookk.title);
book.set("description", bookk.Description);
book.set("registrationId", bookk.RegistrationId);
var newAcl = new Parse.ACL(Parse.User.current());
newAcl.setPublicReadAccess(true);
book.setACL(newAcl);
book.save(null, {
success: function (book) {
// The object was saved successfully.
callback(true, null);
},
error: function (book, error) {
// The save failed.
// error is a Parse.Error with an error code and description.
callback(false, error);
}
});
}
};
return ParseService;
});
And my test so far look like this:
describe('Controller: AddBookCtrl', function() {
// // load the controller's module
beforeEach(module('BookCrossingApp'));
var AddBookCtrl, scope, book;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope;
book = {title: "fooTitle13"};
AddBookCtrl = $controller('AddBookCtrl', {
$scope: scope
});
}));
it('should call Parse Service method', function () {
//We need to get the injector from angular
var $injector = angular.injector([ 'DataServices' ]);
//We get the service from the injector that we have called
var mockService = $injector.get( 'ParseService' );
mockService.registerBook = jasmine.createSpy("registerBook");
scope.registerNewBook(book);
//With this call we SPY the method registerBook of our mockservice
//we have to make sure that the register book have been called after the call of our Controller
expect(mockService.registerBook).toHaveBeenCalled();
});
it('Dummy test', function () {
expect(true).toBe(true);
});
});
Right now the test is failing:
Expected spy registerBook to have been called.
Error: Expected spy registerBook to have been called.
What I am doing wrong?
What I was doing wrong is not injecting the Mocked Service into the controller in the beforeEach:
describe('Controller: AddBookCtrl', function() {
var scope;
var ParseServiceMock;
var AddBookCtrl;
// load the controller's module
beforeEach(module('BookCrossingApp'));
// define the mock Parse service
beforeEach(function() {
ParseServiceMock = {
registerBook: function(book) {},
getBookRegistrationId: function() {}
};
});
// inject the required services and instantiate the controller
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
AddBookCtrl = $controller('AddBookCtrl', {
$scope: scope,
DataService: ParseServiceMock
});
}));
it('should call registerBook Parse Service method', function () {
var book = {title: "fooTitle"}
spyOn(ParseServiceMock, 'registerBook').andCallThrough();
//spyOn(ParseServiceMock, 'getBookRegistrationId').andCallThrough();
scope.registerNewBook(book);
expect(ParseServiceMock.registerBook).toHaveBeenCalled();
//expect(ParseServiceMock.getBookRegistrationId).toHaveBeenCalled();
});
});
You can inject your service and then use spyOn.and.returnValue() like this:
beforeEach(angular.mock.module('yourModule'));
beforeEach(angular.mock.inject(function($rootScope, $controller, ParseService) {
mock = {
$scope: $rootScope.$new(),
ParseService: ParseService
};
$controller('AddBookCtrl', mock);
}));
it('should call Parse Service method', function () {
spyOn(mock.ParseService, "registerBook").and.returnValue({id: 3});
mock.$scope.registerNewBook();
expect(mock.ParseService.registerBook).toHaveBeenCalled();
});
Following Javito's answer 4 years after-the-fact. Jasmine changed their syntax in 2.0 for calling through to real methods on spies.
Change:
spyOn(ParseServiceMock, 'registerBook').andCallThrough();
to:
spyOn(ParseServiceMock, 'registerBook').and.callThrough();
Source
Include angular-mocks.js in your project and read carefully through the following link.

Injecting a mock service for an angularjs controller test

I'm trying to test a controller that depends on a service I built myself. I'd like to mock this service since the service talks to the DOM.
Here's my current test:
describe('Player Controllers', function () {
beforeEach(function () {
this.addMatchers({
toEqualData: function (expected) {
return angular.equals(this.actual, expected);
}
});
});
describe('TestPSPlayerModule', function () {
var $httpBackend, scope, ctrl;
beforeEach(module('PSPlayerModule'));
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
ctrl = $controller(PlayerController, { $scope: scope });
}));
it('should request a clip url from the server when clipClicked is called', function () {
expect(1).toBe(1);
});
});
});
My controller looks like this:
w.PlayerController = function ($scope, $http, $window, speedSlider, $location) {
...
}
so it's the speedSlider I want to mock.
I had the idea to use a module I created in my test code that could provide a faked implementation of the speed slider, so I added the following to the top of the test.js file:
module('TestPSPlayerModule', []).factory('speedSlider', function () {
return = {
...
};
});
and then list that module in the beforeEach() call instead of the concrete one, but if I do that I get the following error:
Injector already created, can not register a module!
So I figure there must be a better way for me to provide a mock implementation of one of my services. Something I can perhaps use sinon.js for....
Also be sure you're not trying to do this inside an inject function call:
This will throw the error:
beforeEach(inject(function(someOtherService) {
module('theApp', function($provide) {
myMock = {foo: 'bar'};
$provide.value('myService', myServiceMock);
someOtherService.doSomething();
});
}));
This will not:
beforeEach(function() {
module('theApp', function($provide) {
myMock = {foo: 'bar'};
$provide.value('myService', myServiceMock);
});
inject(function(someOtherService) {
someOtherService.doSomething();
});
});
Make sure when you use module after its definition that you don't have the extra brackets.
So module('TestPSPlayer') instead of module('TestPSPlayer',[]).
In my case this didn't worked:
beforeEach(module('user'));
beforeEach(inject(function ($http) {
}));
beforeEach(module('community'));
beforeEach(inject(function ($controller, $rootScope) {
}));
I've changed to this to make it to work:
beforeEach(module('user'));
beforeEach(module('community'));
beforeEach(inject(function ($http) {
}));
beforeEach(inject(function ($controller, $rootScope) {
}));
If your provider does not use global init you can use the original injected provider and mock it.
in the example below the testedProvider is your controller.
var injectedProviderMock;
beforeEach(function () {
module('myModule');
});
beforeEach(inject(function (_injected_) {
injectedProviderMock = mock(_injected_);
}));
var testedProvider;
beforeEach(inject(function (_testedProvider_) {
testedProvider = _testedProvider_;
}));
it("return value from injected provider", function () {
injectedProviderMock.myFunc.andReturn('testvalue');
var res = testedProvider.executeMyFuncFromInjected();
expect(res).toBe('testvalue');
});
//mock all provider's methods
function mock(angularProviderToMock) {
for (var i = 0; i < Object.getOwnPropertyNames(angularProviderToMock).length; i++) {
spyOn(angularProviderToMock,Object.getOwnPropertyNames(angularProviderToMock)[i]);
}
return angularProviderToMock;
}

Resources