Unit test for nested controllers - unit-testing

I'm trying to write a unit test for a controller that's nested, but can't figure out how to mock the same behaviour in my test.
I have 2 controllers:
function FirstController ($scope) {
$scope.childs = [{
title : 'Hello, earth!'
}];
};
function SecondController ($scope) {
$scope.child.title = $scope.child.title + $scope.$index;
};
And in my HTML:
<div data-ng-controller="FirstController">
<div data-ng-repeat="child in childs" data-ng-controller="SecondController">
{{ child.title }}
</div>
</div>
And this works as expected (http://jsfiddle.net/tcayp/1/)
The unittests:
// FirstController
it('Should have childs', function () {
scope = {};
ctrl = new FirstController(scope);
expect(scope.childs.length).toBeGreaterThan(0);
});
// SecondController
it('Should have inherited a child', function () {
scope = {};
ctrl = new SecondController(scope);
expect(scope.child.title).toEqual('Hello, earth!0');
});
In the SecondController-test I can't figure out how to mock the inherit chain from ng-repeat.

Ideally, with unit tests we would like to tests classes (units) in isolation. Testing 2 controller in one test might be too much: a test would become more complex and more brittle.
Taking a closer look at the provided example one might notice that it is really not about testing 2 controllers but rather making sure that data are available in a parent scope. So, focusing on one controller only (SecondController) and the inherited data one would write a test like this:
describe('Testing the SecondController controller', function() {
var $parentScope, $scope, ctrl;
it('should prepare title', inject(function($rootScope, $controller) {
//setup hierarchy of scopes with data
$rootScope.childs = [{
title : 'Hello, earth!'
}];
$scope = $rootScope.$new();
$scope.$index = 1;
ctrl = $controller('SecondController', {
$scope: $scope
});
expect($scope.childs[0].title).toEqual('Hello, earth!1');
}));
});
Here is the full jsFiddle: http://jsfiddle.net/pkozlowski_opensource/h8xry/13/
I would really advise against testing 2 controllers together but just for the sake of answering the question, it is possible as well:
describe('Testing the SecondController controller', function() {
it('should prepare title', inject(function($rootScope, $controller) {
$controller('FirstController', {
$scope: $rootScope
});
var $scope = $rootScope.$new();
$scope.$index = 1;
ctrl = $controller('SecondController', {
$scope: $scope
});
expect($scope.childs[0].title).toEqual('Hello, earth!1');
}));
});
And the jsFiddle: http://jsfiddle.net/pkozlowski_opensource/4Qy6b/1/

AngularJS documentation suggests testing nested controllers by instantiating each of them and establishing the same scope hierarchy between them as in your app. This makes sense because (up to a point) you want to test your controller in a realistic context.

In your test instantiate the parent controller with a new scope:
mainScope = $rootScope.$new();
$controller('ParentController', {$scope: mainScope});
and in your child controller, instantiate a new scope using the previously instantiated scope:
childScope = mainScope.$new();
$controller('ChildController', {$scope: childScope});
Example from the AngularJS documentation:
describe('state', function() {
var mainScope, childScope, grandChildScope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
mainScope = $rootScope.$new();
$controller('MainController', {$scope: mainScope});
childScope = mainScope.$new();
$controller('ChildController', {$scope: childScope});
grandChildScope = childScope.$new();
$controller('GrandChildController', {$scope: grandChildScope});
}));
it('should have over and selected', function() {
expect(mainScope.timeOfDay).toBe('morning');
expect(mainScope.name).toBe('Nikki');
expect(childScope.timeOfDay).toBe('morning');
expect(childScope.name).toBe('Mattie');
expect(grandChildScope.timeOfDay).toBe('evening');
expect(grandChildScope.name).toBe('Gingerbread Baby');
});
});

Related

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

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

How to unit test angularjs controller with $location service

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

How to mock an function of an Angular service in Jasmine

I have the following angular controller
function IndexCtrl($scope, $http, $cookies) {
//get list of resources
$http.get(wtm.apiServer + '/v1/developers/me?access_token=' + $cookies['wtmdevsid']).
success(function(data, status, headers, config) {
// snip
}).
error(function(data, status, headers, config) {
// snip
});
$scope.modal = function() {
// snip
}
return;
}
What I am trying to do is mock the get method on the $http service. Here's my unit test code:
describe('A first test suite', function(){
it("A trivial test", function() {
expect(true).toBe(true);
});
});
describe('Apps', function(){
describe('IndexCtrl', function(){
var scope, ctrl, $httpBackend;
var scope, http, cookies = {wtmdevsid:0};
beforeEach(inject(function($injector, $rootScope, $controller, $http) {
scope = $rootScope.$new();
ctrl = new $controller('IndexCtrl', {$scope: scope, $http: $http, $cookies: cookies});
spyOn($http, 'get');
spyOn(scope, 'modal');
}));
it('should create IndexCtrl', function() {
var quux = scope.modal();
expect(scope.modal).toHaveBeenCalled();
expect($http.get).toHaveBeenCalled();
});
});
});
When I run this I get
ReferenceError: wtm is not defined.
wtm is a global object and of course it wouldn't be defined when I run my test because the code that it is declared in is not run when I run my test. What I want to know is why the real $http.get function is being called and how do I set up a spy or a stub so that I don't actually call the real function?
(inb4 hating on globals: one of my coworkers has been tasked with factoring those out of our code :) )
You need to wire up the whenGET method of your $httpBackend in advance of your test. Try setting it up in the beforeEach() function of your test... There is a good example here under "Unit Testing with Mock Backend".
I suggest all globals used the way you described here should be used through the $window service.
All global variables that are available, such as as window.wtm, will also be available on $window.atm.
Then you can stub out your wtm reference completely and spy on it the same way you already described:
var element, $window, $rootScope, $compile;
beforeEach(function() {
module('fooApp', function($provide) {
$provide.decorator('$window', function($delegate) {
$delegate.wtm = jasmine.createSpy();
return $delegate;
});
});
inject(function(_$rootScope_, _$compile_, _$window_) {
$window = _$window_;
$rootScope = _$rootScope_;
$compile = _$compile_;
});
});
Maybe you could create a custom wrapper mock around $httpBackend that handles your special needs.
In detail, Angular overwrites components of the same name with a last-come first-served strategy, this means that the order you load your modules is important in your tests.
When you define another service with the same name and load it after the first one, the last one will be injected instead of the first one. E.g.:
apptasticMock.service("socket", function($rootScope){
this.events = {};
// Receive Events
this.on = function(eventName, callback){
if(!this.events[eventName]) this.events[eventName] = [];
this.events[eventName].push(callback);
}
// Send Events
this.emit = function(eventName, data, emitCallback){
if(this.events[eventName]){
angular.forEach(this.events[eventName], function(callback){
$rootScope.$apply(function() {
callback(data);
});
});
};
if(emitCallback) emitCallback();
}
});
This service offers the exact same interface and behaves exactly like the original one except it never communicates via any socket. This is the service we want to use for testing.
With the load sequence of angular in mind, the tests then look like this:
describe("Socket Service", function(){
var socket;
beforeEach(function(){
module('apptastic');
module('apptasticMock');
inject(function($injector) {
socket = $injector.get('socket');
});
});
it("emits and receives messages", function(){
var testReceived = false;
socket.on("test", function(data){
testReceived = true;
});
socket.emit("test", { info: "test" });
expect(testReceived).toBe(true);
});
});
The important thing here is that module('apptasticMock') gets executed after module('apptastic'). This overwrites the original socket implementation with the mocked one. The rest is just the normal dependency injection procedure.
This article I wrote could be interesting for you, as it goes into further details.