AngularJS Unit Testing Controller w/ service dependency in Jasmine - angularjs

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. :)

Related

Unit-testing an Angular Controller's initialization function

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).

Using Angular to Inject into non-Angular Objects

Is there a way to provide a non-Angular injection target to the Angular $injector such that Angular constructs like $http, $scope, $location or $q can be injected into it?
//non-angular injection container
var injectionTarget= {
$http:undefined,
$scope:undefined
}
//means to inject into target - this is the part in question
var injector = angular.injector();
injector.injectInto( injectionTarget, ["$http", "$scope"]);
I'm having the hardest time finding any info on how to accomplish what I would assume is a very sought-after feature.
I think that probably the easiest way to do this would be to register your objects as services with the module.
var myObject = {} //Defined elsewhere or here as empty
app.service(‘aReferenceName’, function($http){
myObject.$http = $http;
return myObject;
});
This would have the double effect of setting the properties you want on your object, and making it accessible from angular as needed. It's also a pretty simple block of code. Note the implication though that as a service it would be a singleton from angular's perspective. If you need to do it as a class with many instances, you'll be wanting a factory.

Updating a view in AngularJS using $watch on a service

I'm a newbie to Angular and I'm struggling to understand how views are updated with scope changes. I am trying to update a header in my app using Angular JS based on whether a user is logged in. This information is returned by a Login service.
I have distilled my problem into two plunkers, one working and one not.
In order to get it to work I have to assign my LoginService to a variable on the scope of the HeaderCtrl.
angular.module('app', [])
.controller('HeaderCtrl', function ($scope, LoginService) {
$scope.loginService = LoginService;
$scope.$watch('loginService.isLoggedIn()', function(newVal) {
$scope.isLoggedIn = newVal;
});
Here is the working version
http://plnkr.co/edit/KBzE9N?p=preview
Now if I remove the reference to the LoginService in the scope of the HeaderCtrl and just use the injected service in the watch directly, the view stops updating. That is demonstrated here
http://plnkr.co/edit/IjFS2w?p=preview
Can anyone explain to me why the second case doesn't work? I've also read that it is a bad idea to have watches inside a controller so I'm open to better solutions.
Because the $watch watches scope variables in the scope you are calling it from. In your second example, $watch is looking for a LoginService variable in your scope, which of course does not exists.

AngularJS: unknown provider until after page loads?

So this is really weird, maybe it has a simple answer I'm missing. The following code gives an unknown provider error:
var foo = angular.module('foo', [ 'ngRoute', 'ngAnimate', 'ngCookies' ]);
foo.factory('fooApi', function ($scope, $http) {
var url = '/api/';
var factory = {};
factory.action = function (fields) {
fields.userid = $scope.userid;
fields.token = $scope.token;
console.log(JSON.stringify(fields));
return $http.post(url, { data: fields });
};
return factory;
})
.controller('loginController', function ($scope, fooApi) {
// do stuff
});
It's all loading together in the same file, and I'd think the factory being first would resolve and the injector would be able to find it when referenced below. But it gives an unknown provider error.
However, if I comment out the controller and wait for the page to load and then do the exact same controller declaration in the Chrome JS console it works fine.
Anyone run into this before and know how to deal with it? I haven't been able to find this same exact issue anywhere.
Like #tasseKATT said, you can not inject $scope into a service, particularly a factory. Maybe your confusion is because $scope can be injected in controllers, so you tried to injected into a factory.
An interesting thing is that the $scope that you see being injected into controllers is not a service - like the rest of the injectable stuff -, but is a Scope object.
The main purpose of $scope is a king of glue between views and controllers, it doesn't make much sense to pass a $scope into a service.
The services only have access to the $rootScope service.
If you need to pass the $scope of a specific controller to a service always you can pass it like parameter of a function in the service. This approach is not recommended because starting to break the SoC and the single responsibility principle, but maybe could fit you.
Good luck :-)

DOM not updated after scope modification in handler of async service within directive

My directive calls some async service based on $http. Service name comes to derectiva as a parameter:
.directive('lookup', ['$scope', function($scope, svc) {
var injector = angular.injector(['app', 'ng']);
var svc = injector.get(scope.serviceName);
...
$scope.clickHandler = function () {
svc.lookup($scope.inputText).then(function (res) {
$scope.inputText = res.data.name;
});
}
...
})
While setting chrome breakpoint inside then() handler we can see by call stack that it's fired within $apply(), so every scope change should be reflected in DOM. $q documentation also states that handlers should be synchronized with scope/dom update cycle, but my view is not updated after clickHandler(). Calling explicit scope.$digest() after scope change does the thing, but don't know why it does not work withouth it. Another strange thing I've noticed is that on the breakpoint $scope has no parent-child relation with $rootScope for which http response handler is wrapped by $apply(). Maybe this is the reason of my troubles. Am I doing something wrong?
Real code is hard to cite, but see essential example code on plunker
Explanation:
The villain of the piece is the use of angular.injector in the link function:
var injector = angular.injector(['ng', 'app']);
var lookupService = injector.get(scope.serviceName);
This code will create a new injector function instead of retrieving the one already created for your application when it was bootstrapped.
Services in Angular are singletons in the sense that they are only created once per injector. In your case this means that injector.get(scope.serviceName) will return a new instance of the service and not the same instance as you possibly would've interacted with before.
The same goes for the $http service that your lookupService uses. In turn, $http injects $rootScope to trigger the digest cycle by calling $apply on it after a XHR request. But as the $rootScope will not be the same as for the rest of your application, the digest cycle will be in vain.
This is also the reason for the behaviors you are witnessing, and why an explicit call to scope.$digest() helps, as it will trigger the digest cycle on the correct scope chain.
Solution:
Inject the $injector service in your directive and use that to retrieve the service you want:
.directive('lookup', ['$injector',
function($injector) {
function link(scope, elem) {
var lookupService = $injector.get(scope.serviceName);
Demo: http://plnkr.co/edit/nubMohsbHAEkbYSg39xV?p=preview

Resources