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
Related
We can create functions at scope level. If i have a controller todoCtrl then if i define a function inside the controller scope that method will reside inside the controller scope.
For example,
$scope.scopeMet = function() {}
We can also define methods at window level using $window service of angular.
For example,
$window.onSignIn = function() {}
Google Sign in callback is captured at window level, hence, i was curious to know what is the meaning and the difference between the two illustrations given above ?
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. :)
everywhere in application my company created we used this example of how to create a controller :
app.myFunnyController = function($scope.....){}
but i see that everywhere in test people are using this way of creating controllers:
app.controller('myFunnyController', function ($scope) {
}
And i can see that when i am creating my test and using app.myFunnyController declaration:
'use strict';
describe('publicCtrl', function(){
beforeEach(module('app'));
it("should be true", inject(function($controller){
var scope = {},
ctrl = $controller('myFunnyController', {$scope : scope});
expect(scope.data).toBe("test2");
}));
})
I getting an error of myFunnyController is not a function. If i using the second type of declaration, everything works fine. Why does this happend?
An other problem is that i am getting error: scope is not defined.
I am new to Karma and Unit testing for front end, what am i doing wrong?
From my understanding, the second syntax (app.controller(...)) registers the controller function on the module. The first syntax just adds the controller function as an attribute of the module. app.controller does a bit more magic under the hood so that when you call $controller('myFunnyController, ...) in your test, the module knows about the controller and can run it. This is the recommended way to define controllers according to the angular controllers guide.
I am currently working on a tutorial that integrates angular JS into a Rails app.
Tests are setup as follows:
describe( 'Club functionality', function() {
// mock Application to allow us to inject our own dependencies
beforeEach(angular.mock.module('league'));
// create the custom mocks on the root scope
beforeEach(angular.mock.inject(function($rootScope, _$httpBackend_, $state){
//create an empty scope
scope = $rootScope.$new();
// we're just declaring the httpBackend here, we're not setting up expectations or when's - they change on each test
scope.httpBackend = _$httpBackend_;
scope.$state = $state;
}));
afterEach(function() {
scope.httpBackend.verifyNoOutstandingExpectation();
scope.httpBackend.verifyNoOutstandingRequest();
});
...
After finishing that section of the tutorial and browsing some of the Angular docs it is still not clear to me why the underscores are used when including the $httpBackend dependency. Why is this mocked out like so? scope.httpBackend = _$httpBackend_;
This one is more simple than it looks.
For convenience, we want to reference our services / scopes across our test suite as we are used to in our applications. So we need to save their reference at the outer function scope.
First we need to inject them so we try to do so without underscores like so:
var $httpBackend;
beforeEach(angular.mock.inject(function( $httpBackend ){
The problem is that the inner function scope variable $httpBackend shadows the outer function scope variable $httpBackend, so we cannot go up the scope chain to set our reference outside.
To fix it, we must have different names for the inner and the outer scope variables. The underscores are just a little help from the $injector to do it without pain.
DISCLAIMER: I did not write this answer. It was copied from here.
So the key to the "secret" is here:
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L57
Basically $injector will strip leading / trailing underscores when
inspecting function's arguments (to retrieve dependencies).
This is useful trick since we can do $rootScope = _$rootScope_; and
then, later in the tests use $rootScope. It kind of looks nicer since
test code uses the same variables as a controller would use. At
somehow having leading $ helps to remember that those are injected
variables.
Of course we could do: rootScope = $rootScope; but it wouldn't be so
obvious that rootScope was injected.
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