Unit-testing an Angular Controller's initialization function - angularjs

I am faced with what is I believe a very straightforward scenario, but I cannot find a clear answer: I have a controller which does a number of things when created, including somewhat complicated stuff, and so I have created an initialization function to do it.
Here's what the controller code looks like:
function MyCtrl() {
function init() {
// do stuff
}
var vm = this;
vm.init = init;
vm.init();
}
Obviously, I want to unit test init(), but I cannot find a way to do so: when I instanciate the controller in my tests, init() is run once, which makes it hard to correctly test its side-effects when I run it a second time.
I'm using karma-jasmine for the tests, and usually do something like this:
describe('Controller: MyCtrl', function () {
var myCtrl;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function ($controller, $rootScope) {
$scope = $rootScope.$new();
createController = function () {
return $controller('MyCtrl', {$scope: $scope});
};
}));
it('bleh', function() {
myCtrl = createController();
// init has already been run at that point
});
)};
Again, I'm sure it's really straightforward and I'm simply missing the point, but I'm still fairly new to Angular. Thanks a lot for your help!

Putting JB Nizet's answer here instead of in a comment.
Many thanks for answering!
It's a chicken and egg problem: init is called by the constructor, and you need to call the constructor in order to call your init function(s). I still maintain that the fact that you have one or several "private" functions called by the constructor is an implementation detail. What you really want to test is that the constructor does its job. You could simplify that by delegating to service functions (that could be unit tested, and mocked when testing the controller).

Related

AngularJS Unit Testing Controller w/ service dependency in Jasmine

I'm brand new to testing, and I've been trying to find the best strategy for unit testing an AngularJS controller with a service dependency. Here's the source code:
app.service("StringService", function() {
this.addExcitement = function (str) {
return str + "!!!";
};
});
app.controller("TestStrategyController", ["$scope", "StringService", function ($scope, StringService) {
$scope.addExcitement = function (str) {
$scope.excitingString = StringService.addExcitement(str);
};
}]);
And the test I'm using currently:
describe("Test Strategy Controller Suite", function () {
beforeEach(module("ControllerTest"));
var $scope, MockStringService;
beforeEach(inject(function ($rootScope, $controller) {
$scope = $rootScope.$new();
MockStringService = jasmine.createSpyObj("StringService", ["addExcitement"]);
$controller("TestStrategyController", {$scope: $scope, StringService: MockStringService});
}));
it("should call the StringService.addExcitement method", function () {
var boringString = "Sup";
$scope.addExcitement(boringString);
expect(MockStringService.addExcitement).toHaveBeenCalled();
});
});
This test passes, but I'm confused about something: if I change the name of the method in the service (let's say I call it addExclamations instead of addExcitement but not where it is used in the controller (still says $scope.excitingString = StringService.addExcitement(str);), my tests still pass even though my controller is now broken. However, once I change the method name in the controller as well, so as to fix the actual breakage caused by changing the service's method name, my tests break because it's trying to call the old addExcitement method.
This would indicate that I would need to manually keep the method names in sync with the service by changing the jasmine spy object line to MockStringService = jasmine.createSpyObj("StringService", ["addExclamations"]);.
All of this seems backwards to me, since I feel like my test should break when I change the service's method name without changing how the controller references that service name. But I'm not sure how to get the best of both worlds here, because if I'm expecting my test to keep track of that service name somehow, there's no way for it to pass again when I change the method name in both the service and the controller because the spyObj still has the old name.
Any insight or advice about the strategy behind this would be greatly appreciated. I'm going to be teaching this to some students, and am mostly trying to make sure I'm following best practices with this.
I'd say that's the expected result of the way your test code works, simply because you created a "brand new" mock service object. I guess you know what I am talking about.
What I usually do is get the service instance and mock the method, instead of creating a completely new mock object.
beforeEach(inject(function ($rootScope, $controller, $injector) {
$scope = $rootScope.$new();
MockStringService = $injector.get('StringService');
spyOn(MockStringService , 'addExcitement').andReturn('test');
$controller("TestStrategyController", {$scope: $scope, StringService: MockStringService});
}));
please note that andReturn() is a jasmine 1.x method, depends on the version you are using, you may want to change the code a little bit.
Having it this way, if you change the method name in StringService, you should get errors from spyOn() method, as the method doesn't not exist any more.
Another thing is that, you don't have to use $injector as I did to get the service instance, you can just inject your service instead in fact. I don't recall why I did it this way. :)

ngMock injecting $scope local into controller

This is a continuation of another question I asked that was successfully answered.
I'm learning how to unit test AngularJS applications with Karma, Jasmine, and ngMock. Here's the code I have a question about:
describe('myController function', function() {
describe('myController', function() {
var scope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
// These are the 2 lines I'm a bit confused about:
scope = $rootScope.$new();
$controller('MyController', {$scope: scope});
}));
it("...");
});
});
Question 1: Why do we create a new scope and include it in the locals injection area in this line: $controller('MyController', {$scope: scope});? It seems to work just fine (as in, scope now represents the $scope object from that controller and has all the properties and functions it's supposed to), but the code seems to imply that we're resetting the controller's $scope with our newly-created (and empty) scope (from the line scope = $rootScope.$new();). So I think I'm just not fully understanding the purpose/inner-workings of those locals.
Question 2: I also have seen from my searching that new scope gets created in 2 common ways, either the way shown above with $rootScope.$new(), or by simply declaring scope = {}. Even the Angular docs do it both ways, as seen here (using $rootScope.$new()) and here (using $scope = {}. Why do this one way over another? Is there a difference?
Why do we create a new scope
There are a couple of reasons that come to mind, but the short answer is that you don't have to if you don't want to. You can just as easily pass in $rootScope and your tests will still work. It is also just typically done because it is more in line with what actually happens in angular - the $scope that the controller is injected with is likely a descendent of $rootScope.
What is $controller('MyController', {$scope: scope}); doing?
The ngMock module provides the $controller function as a sort of constructor for creating your controller - well, technically it's a decorator to the $controller function in the ng module, but that's not important. The first argument is typically a string, but can be a function.
If called with a function then it's considered to be the controller constructor function. Otherwise it's considered to be a string which is used to retrieve the controller constructor...
The second argument are the "locals" to be injected into the controller during creation.
So, in your example, you are saying you want to create the "MyController" controller, and you want to inject your scope variable as the argument named $scope in the controllers function.
The whole point of this is to inject your own version of the scope into the controller, but a version that you created in your test, so that you can assert the different things that happen to the scope because of the controller.
This is one of the benefits of dependency injection.
Examples
If the following makes sense:
var scope = {};
scope.add = function(){};
expect(typeof scope.add).toBe('function');
then let's take it one step further and move the adding of the function into another function:
var addFunctionToScope = function(scope) {
scope.add = function(){};
};
var scope = {};
addFunctionToScope(scope);
expect(typeof scope.add).toBe('function');
Now just pretend your new function to add the function to the scope is really just called $controller, instead of "addFunctionToScope".
var scope = {};
$controller('SomeController', {$scope: scope});
expect(typeof scope.add).toBe('function');
Why use $rootScope.$new() over {}
Again, you can use either. However, if you plan on using some of the scope specific functions, like $on, or $new, or $watch, etc - you will want to inject an actual scope object, created using the $new function on an existing scope.
Docs
scope
ngMock $controller
ng $controller

How to: mock module dependencies

Tearing my hair out on this one.
I have the following module/controller I wish to test
angular
.module('monitor.tableLord.controller', ['monitor.wunderTable'])
.controller('TableLordController', TableLordController);
TableLordController.$inject = ['$scope', 'historyState'];
function TableLordController($scope, historyState) {
....some code....
}
The module monitor.wunderTable contains a directive that should be loaded before the controller, but the controller I want to test does not actually depend on monitor.wunderTable. monitor.wunderTable does however have a LOT of other dependencies....
My testfile:
describe('TableLordController', function() {
var $scope, historyState;
beforeEach(function() {
angular.module('monitor.wunderTable', []);
module('monitor.tableLord.controller');
});
beforeEach(angular.mock.inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('TableLordController', {$scope: $scope, historyState: {}});
}));
it('loads', function() {
$scope.$digest();
});
});
For some reason (I didn't think this should be possible), my mock version of monitor.wunderTable is interfering with the tests i have for this module. Every test of the controller defined in this module now fails with: "Argument 'WunderTableController' is not a function, got undefined".
I case it is relevant, here is my the definition of monitor.wunderTable:
angular
.module('monitor.wunderTable', [
'ui.grid',
'ui.grid.infiniteScroll',
'ui.grid.autoResize',
'monitor.wunderTable.service'
])
.controller('WunderTableController', WunderTableController)
.directive('wunderTable', wunderTable);
function wunderTable(){...}
WunderTableController.$inject = [];
function WunderTableController(...){...}
Edit: Posts suggesting that I remove the module dependency (as it is not strictly needed) will not be accepted as correct answer (and possibly downwoted).
Your confusion comes from misunderstanding how modules work in Angular. Modules are stored inside angular. Once they are overriden, they are overriden for the current test run, not for the current spec. Module mocking isn't supported by ngMock (it would require some substantial changes in Angular core) and beforeEach won't help anything.
Unless you want to run test suites in separate runs, the solution is to backup the module before mocking it. In some cases angular.extend and angular.copy are unable to handle complex objects properly. Object.assign, jQuery.extend or node-extend may be better candidates. In the case of extends deep copy can also used if necessary.
So in one test suite it is
var moduleBackup = angular.module('monitor.wunderTable');
angular.module('monitor.wunderTable', []);
describe('TableLordController', function() {
...
And in another
Object.assign(angular.module('monitor.wunderTable'), moduleBackup);
describe('WunderTableController', function() {
...
The good thing about TDD is that it clearly indicates the flaws in app design and teaches the developer to write test-friendly code in immediate and ruthless manner. Module dependency implies that the components inside the module depend on the components from another one. If they doesn't or they are coupled too tightly, this can be considered a potential flaw and the subject for refactoring.
The fact that the solution is hack-ish and tends to break makes it unfit for testing.
TL;DR: Yes, you have to remove the module dependency.

Any difference between $injector and Inject() in AngularJS?

Is there any difference/preference between:
inject(function($injector) {
rootScope = $injector.get('$rootScope');
});
And
inject(function($rootScope){
rootScope = $rootScope;
});
Are the eqal as far as getting a resource injected into a test in Jasmine?
From the documentation on the inject function:
The inject function wraps a function into an injectable function. The
inject() creates new instance of $injector per test, which is then
used for resolving references.
So, to answer your question, no, there really isn't a difference in the two ways, other than (in my opinion) it's a lot easier to just use the inject function to get dependencies instead of going through the $injector

What does the injector does that global variables can't?

so in the AngularJS docs I saw this about the injector:
// You write functions such as this one.
function doSomething(serviceA, serviceB) {
// do something here.
}
// Angular provides the injector for your application
var $injector = ...;
///////////////////////////////////////////////
// the old-school way of getting dependencies.
var serviceA = $injector.get('serviceA');
var serviceB = $injector.get('serviceB');
// now call the function
doSomething(serviceA, serviceB);
///////////////////////////////////////////////
// the cool way of getting dependencies.
// the $injector will supply the arguments to the function automatically
$injector.invoke(doSomething); // This is how the framework calls your functions
it looks nice. but i don't get it. in the last line where the injector looks for dependencies, isn't it exactly like having global variables serviceA, serviceB? I mean, say I would rewrite it like this:
var serviceA, serviceB;
function doSomething() {
// access serviceA, serviceB
}
what is the benefit of having the injector do that? I mean, if he can magically find the right objects for the arguments, doesn't it mean I can find them just as easily if they are global variables?
I hope my question is clear...
BTW, if you're doing something that genuinely needs to be at a lower level than $scope, Angular has you covered:
http://docs.angularjs.org/api/ng.$rootScope
The docs for $rootScope are a little light, but generally speaking, you inject it and use it the same as regular $scope. It is the shared root scope of the app you're in.
The only reason to use Window scope would be to communicate between multiple Angular apps. And even then...you got it, Angular has you covered:
http://docs.angularjs.org/api/ng.$window

Resources