How to unit test angularjs controller with $location service - unit-testing

I am trying to create a simple unit test that tests my show function.
I get the following error:
TypeError: Object #<Object> has no method 'show'
It seems like $rootScope isn't the scope of the controller?
Here's my controller:
function OpponentsCtrl($scope, $location) {
$scope.show = function(url) {
$location.path(url);
}
}
OpponentsCtrl.$inject = ['$scope', '$location'];
Here's my controller unit test:
describe('OpponentsCtrl', function() {
beforeEach(module(function($provide) {
$provide.factory('OpponentsCtrl', function($location){
// whatever it does...
});
}));
it('should change location when setting it via show function', inject(function($location, $rootScope, OpponentsCtrl) {
$location.path('/new/path');
$rootScope.$apply();
expect($location.path()).toBe('/new/path');
$rootScope.show('/test');
expect($location.path()).toBe('/test');
}));
});

This is how my test ended up working.
describe('OpponentsCtrl', function() {
var scope, rootScope, ctrl, location;
beforeEach(inject(function($location, $rootScope, $controller) {
location = $location;
rootScope = $rootScope;
scope = $rootScope.$new();
ctrl = $controller(OpponentsCtrl, {$scope: scope});
}));
it('should change location when setting it via show function', function() {
location.path('/new/path');
rootScope.$apply();
expect(location.path()).toBe('/new/path');
// test whatever the service should do...
scope.show('/test');
expect(location.path()).toBe('/test');
});
});

Why don't you simply use a spyOn function?
describe('OpponentsCtrl', function() {
var location;
beforeEach(module(function($provide) {
$provide.factory('OpponentsCtrl', function($location){
location = $location;
});
}));
it('should change location when setting it via show function', inject(function() {
spyOn(location, 'path');
expect(location.path).toHaveBeenCalledWith('/new/path');
}));
});
Hope this helps!

I prefer to mock location and services as then it's a unit (not integration) test:
'use strict';
describe('flightController', function () {
var scope;
var searchService;
var location;
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
mockSearchService();
mockLocation();
createController($controller);
}));
it('changes location to month page', function () {
searchService.flightToUrl.and.returnValue('Spain/Ukraine/December/1');
scope.showMonth();
expect(location.url).toHaveBeenCalledWith('search/month/Spain/Ukraine/December/1');
});
function mockSearchService() {
searchService = jasmine.createSpyObj('searchService', ['flightToUrl']);
}
function mockLocation() {
location = jasmine.createSpyObj('location', ['url']);
}
function createController($controller) {
$controller('flightController', {
$scope: scope,
searchService: searchService,
$location: location
});
}
});
Cheers

Related

Angular unit-test controllers - mocking service inside controller

I have the following situation:
controller.js
controller('PublishersCtrl',['$scope','APIService','$timeout', function($scope,APIService,$timeout) {
APIService.get_publisher_list().then(function(data){
});
}));
controllerSpec.js
'use strict';
describe('controllers', function(){
var scope, ctrl, timeout;
beforeEach(module('controllers'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new(); // this is what you missed out
timeout = {};
controller = $controller('PublishersCtrl', {
$scope: scope,
APIService: APIService,
$timeout: timeout
});
}));
it('should have scope variable equals number', function() {
expect(scope.number).toBe(3);
});
});
Error:
TypeError: Object #<Object> has no method 'get_publisher_list'
I also tried something like this, and it didn't work:
describe('controllers', function(){
var scope, ctrl, timeout,APIService;
beforeEach(module('controllers'));
beforeEach(module(function($provide) {
var service = {
get_publisher_list: function () {
return true;
}
};
$provide.value('APIService', service);
}));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
timeout = {};
controller = $controller('PublishersCtrl', {
$scope: scope,
APIService: APIService,
$timeout: timeout
}
);
}));
it('should have scope variable equals number', function() {
spyOn(service, 'APIService');
scope.get_publisher_list();
expect(scope.number).toBe(3);
});
});
How can i solve this? any suggestions?
There are two ways (or more for sure).
Imagining this kind of service (doesn't matter if it is a factory):
app.service('foo', function() {
this.fn = function() {
return "Foo";
};
});
With this controller:
app.controller('MainCtrl', function($scope, foo) {
$scope.bar = foo.fn();
});
One way is just creating an object with the methods you will use and spy them:
foo = {
fn: function() {}
};
spyOn(foo, 'fn').andReturn("Foo");
Then you pass that foo as a dep to the controller. No need to inject the service. That will work.
The other way is to mock the service and inject the mocked one:
beforeEach(module('app', function($provide) {
var foo = {
fn: function() {}
};
spyOn(foo, 'fn').andReturn('Foo');
$provide.value('foo', foo);
}));
When you inject then foo it will inject this one.
See it here: http://plnkr.co/edit/WvUIrtqMDvy1nMtCYAfo?p=preview
Jasmine 2.0:
For those that struggle with making the answer work,
as of Jasmine 2.0 andReturn() became and.returnValue()
So for example in the 1st test from the plunker above:
describe('controller: MainCtrl', function() {
var ctrl, foo, $scope;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller) {
foo = {
fn: function() {}
};
spyOn(foo, 'fn').and.returnValue("Foo"); // <----------- HERE
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {$scope: $scope , foo: foo });
}));
it('Should call foo fn', function() {
expect($scope.bar).toBe('Foo');
});
});
(Source: Rvandersteen)

Angularjs Unit Testing: Am I doing it right?

I started to write unit tests for my angular app.
However it seems to me that I use a lot of boilerplate code to init and test the controller.
In this Unit Test I want to test if a model from the scope is sent to the Api when I execute a function.
I needed 20 lines of code for this. This makes it inconvenient to write unit tests that do only one thing.
Do you have any tips on getting the code size to a smaller chunk?
This is my current unit test:
'use strict';
describe('controllers', function(){
beforeEach(module('kronos'));
describe('CustomerSignupCtrl', function() {
it('should send customer to Api on submit', inject(function($controller) {
var scope = {};
var $location = {};
var Api = {
signupCustomer: function(customer) {
expect(customer).toEqual({attrs: "customerdata"});
return {
success: function() { return this; },
error: function() { return this; }
};
}
};
var ctrl = $controller('CustomerSignupCtrl', {
$scope: scope,
$location: location,
Api: Api});
scope.customer = {attrs: "customerdata"};
scope.signup();
}));
});
});
What I don't like in particular are the following points
I need to init the all dependencies and it doesn't matter if I use them or not
The Api returns a promise that I only need because the controller is expecting the promise
I need to init the controller.
How can I make this code shorter and more explicit?
Edit: I just noticed I can ignore the $location Service for this unit test. Great
Edit2: I found out about angular-app, which serves as a good practice example app. There you can find specs with jasmine, which are really nice written.
Use another beforeEach method in your describe scope to set up scope, $location, controller etc, then just change them in your test as you need to. Js is dynamic so all should be fine.
You can also extract each object that you set up into a function so that you can reinitialise them in a test if you need to.
describe('controllers', function(){
beforeEach(module('kronos'));
describe('CustomerSignupCtrl', function() {
var controller, scope, $location, Api;
beforeEach(function(){
scope = {};
$location = {};
Api = {
signupCustomer: function(customer) {
expect(customer).toEqual({attrs: "customerdata"});
return {
success: function() { return this; },
error: function() { return this; }
};
}
};
controller = makeController();
})
function makeController(){
inject(function($controller){
controller = $controller('CustomerSignupCtrl', {
$scope: scope,
$location: location,
Api: Api});
});
}
it('should send customer to Api on submit', function() {
scope.customer = {attrs: "customerdata"};
scope.signup();
});
});
});
You can not shorten your code much. Things like initialization, mocking and assertion have to be done at some place. But you can improve the readability of your code by decoupling initialization and test code. Something like this:
describe('CustomerSignupCtrl', function(){
var controller, scope, location, api;
beforeEach(module('kronos'));
// initialization
beforeEach(inject(function($controller, $rootScope, $location, Api){
scope = $rootScope.$new();
location = $location;
api = Api;
controller = $controller('CustomerSignupCtrl', {
$scope: scope, $location: location, Api: api});
}));
// test
it('should send customer to Api on submit', function() {
scope.customer = {attrs: "customerdata"};
spyOn(api,'signupCustomer').andCallFake(function(customer) {
return {
success: function() { return this; },
error: function() { return this; }
};
});
scope.signup();
expect(api.signupCustomer).toHaveBeenCalledWith(scope.customer);
});
});

Faking a Angular Factory in a directive in jasmine

Question: How do I fake my pointFactory so I can Jasmine Unit Test it.
I have the Following Directive.
It takes the html sends it to a factory and the uses the response for some logic
CommonDirectives.directive('TextEnrichment',['PointFactory','appSettings', function (pointFactory,settings) {
return {
restrict: 'A',
link : function (scope, element, attrs) {
var text = element.html();
pointFactory.getPoints(text).then(function(response){
})}}}]);
So far my unit tests looks like this, however it doesn't work since I'm not injecting the factory.
beforeEach(module('app.common.directives'));
beforeEach(function () {
fakeFactory = {
getPoints: function () {
deferred = q.defer();
deferred.resolve({data:
[{"Text":"Some text"}]
});
return deferred.promise;
}
};
getPointsSpy = spyOn(fakeFactory, 'getPoints')
getPointsSpy.andCallThrough();
});
beforeEach(inject(function(_$compile_, _$rootScope_,_$controller_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Factory to have been Called', function () {
var element = $compile('<div data-text-enrichment=""> Text </div>')($rootScope)
expect(getPointsSpy.callCount).toBe('1');
});
Update
Following advice from Felipe Skinner I have updated the test with the following
beforeEach(function(){
module(function($provide){
$provide.factory('PointFactory',getPointsSpy)
})
});
However I get the following error:
TypeError: 'undefined' is not a function (evaluating
'pointFactory.getPoints(text)')
You can use the $provide to inject your controller dependencies.
Here's my beforeEach for example:
describe('MyCtrl', function() {
var $controller,
$scope,
$httpBackend,
windowMock,
registerHtmlServiceMock,
mixPanelServiceMock,
toastMock;
beforeEach(function() {
windowMock = { navigator: {} };
registerHtmlServiceMock = {};
mixPanelServiceMock = jasmine.createSpyObj('mixpanel', ['track']);
toastMock = jasmine.createSpyObj('toast', ['error']);
module('myModule');
module(function($provide) {
$provide.value('$window', windowMock);
$provide.value('RegisterHtmlService', registerHtmlServiceMock);
$provide.value('MixPanelService', mixPanelServiceMock);
$provide.value('ToastService', toastMock);
});
inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$scope = _$rootScope_.$new();
$controller = _$controller_('CourseSelectionCtrl', { $scope: $scope });
$httpBackend = _$httpBackend_;
});
});
// my test cases
});
I haven't tried mocking a function that returns some value. Those two mocks (mixpanel-track and toast-error) are for "void" functions.
UPDATE:
Try changing the previous $provide with this type of injection then.
Change from this:
module(function($provide) {
$provide.value('$window', windowMock);
$provide.value('RegisterHtmlService', registerHtmlServiceMock);
$provide.value('MixPanelService', mixPanelServiceMock);
});
inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$scope = _$rootScope_.$new();
$controller = _$controller_('CourseSelectionCtrl', { $scope: $scope });
$httpBackend = _$httpBackend_;
});
To this:
beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
mixPanelService = mixPanelServiceMock;
$scope = _$rootScope_.$new();
$controller = _$controller_('MyCtrl', { $scope: $scope, MixPanelService: mixPanelService });
$httpBackend = _$httpBackend_;
}));
The rest of the code should be the same, except for that. Let me know if this works

Unit-testing a controller that uses $http

I have a simple controller and the first thing I need it to do is assign a value to scope.
function TestCtrl($scope, $http) {
$scope.listForms = 'some list';
}
The following test for the controller works as expected:
describe('Testing a controller', function() {
var ctrl, scope, httpMock;
beforeEach(inject(function($injector) {
scope = $injector.get('$rootScope').$new();
ctrl = $injector.get('$controller');
ctrl(TestCtrl, { $scope: scope });
}));
it("assigns to scope", function() {
expect(scope.listForms).toMatch("some list");
});
});
But when I change the function to get the list from my API
function TestCtrl($scope, $http) {
$http.get('/api/listForms').success(function(list) {
$scope.aListOfForms = 'some list';
});
}
and the test changes to
describe('Testing a controller', function() {
var ctrl, scope, httpMock;
beforeEach(inject(function($injector) {
httpMock = $injector.get('$httpBackend');
scope = $injector.get('$rootScope').$new();
httpMock.when('GET', '/tactical/api/listOrderForms').respond("an order form");
ctrl = $injector.get('$controller');
ctrl(TestCtrl, {
$scope: scope,
$http: httpMock
});
}));
it("gets the list from the api and assigns it to scope", function() {
httpMock.expectGET('tactical/api/listOrderForms');
expect(scope.orderFormList).toMatch("an order form");
httpMock.flush();
});
});
I get the following errors:
TypeError: 'undefined' is not a function
Expected undefined to match 'an order form'.
Error: No pending request to flush !
Does anyone know what I am doing wrong? Thanks in advance.
$http uses $httpBackend to talk to external resources. You have mocked $httpBackend, but the controller still needs to talk to it trough $https interface.
This should do it:
describe('Testing a controller', function() {
var ctrl, scope, httpMock;
beforeEach(inject(function($controller, $rootScope, $httpBackend) {
httpMock = $httpBackend;
scope = $rootScope.$new();
httpMock.when('GET', '/tactical/api/listOrderForms').respond("an order form");
ctrl = $controller;
ctrl(TestCtrl, {
$scope: scope
});
}));
it("gets the list from the api and assigns it to scope", function() {
httpMock.expectGET('tactical/api/listOrderForms');
httpMock.flush();
expect(scope.orderFormList).toMatch("an order form");
});
});
you can't replace $http service as $httpBackend service for your controller manually.
Change
ctrl(TestCtrl, {
$scope: scope,
$http: httpMock
});
to
ctrl(TestCtrl, {
$scope: scope
});
It should work.
You need to call httpMock.flush() before the expect(). The flush call simulates the response returning from the "back end," calling the success function that was bound to the http request.

in angular js while testing the controller got Unknown provider

I have the following controller:
angular.module('samples.controllers',[])
.controller('MainCtrl', ['$scope', 'Samples', function($scope, Samples){
//Controller code
}
Which dependent on the following service:
angular.module('samples.services', []).
factory('Samples', function($http){
// Service code
}
Tried to test the controller using the following code:
describe('Main Controller', function() {
var service, controller, $httpBackend;
beforeEach(module('samples.controllers'));
beforeEach(module('samples.services'));
beforeEach(inject(function(MainCtrl, Samples, _$httpBackend_) {
}));
it('Should fight evil', function() {
});
});
But got the following error:
Error: Unknown provider: MainCtrlProvider <- MainCtrl.
P.s Tried the following post, didn't seem to help
The correct way to test controllers is to use $controller as such:
ctrl = $controller('MainCtrl', {$scope: scope, Samples: service});
Detailed example:
describe('Main Controller', function() {
var ctrl, scope, service;
beforeEach(module('samples'));
beforeEach(inject(function($controller, $rootScope, Samples) {
scope = $rootScope.$new();
service = Samples;
//Create the controller with the new scope
ctrl = $controller('MainCtrl', {
$scope: scope,
Samples: service
});
}));
it('Should call get samples on initialization', function() {
});
});