Angular Unit testing with Karma - angularjs

I have an angular app that i am developing tests for. In the app.js file i have something like this:
angular.module('app',
[
'app.config',
'app.factories',
'app.directives',
'app.controllers'
]
);
For each controller i want to be in that controller module i essentially define them like this:
angular.module('app.controllers').controller("controller1" ,[],function(){
bleh bleh bleh
code code code
})
The goal here is to write some unit tests with karma but unfortunately the most i have been able to figure out how to do is make sure the dependencies of my main modules load.
What I need to figure out is using the structure i have, how do I (a) create a test to make sure that my controller is actually there, and (b) test things inside the controller
I have tried multiple ways but cannot seem to instantiate the controller within my test framework.

You can test for the existence of your controller like this:
describe("SomeControllerTest", function () {
var scope, ctrl;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('SomeController', {
$scope: scope
});
}));
it("should be defined", function () {
expect(ctrl).toBeDefined();
});
});
Careful with your controller syntax. The second param is an array of strings ending with the function, the function is not a 3rd param.
app.controller('MyController', [ '$log', function MyController($log) {} ]);

Related

Unit testing controllers in AngularJS (1.5), when those controllers are exported as modules (headache...)

So, I'm trying to run unit tests on a Controller in an Angular 1.5 application, so that I can do TDD for every other controller from now on. But, it's turning into a bit of a headache.
All of the tutorials and blog posts that talk about testing ng-controllers seem to do so by injecting the controller that their app uses, and injecting their actual app.
The application I'm building exports a function for its controllers, and I'm wondering if this is what's causing my headaches here.
Here's an example:
// Controller file
const myController = (app) => {
const controller = app.controller('MyCtrl', ['$scope', 'SomeService', ($scope, SomeService) => {
// Stuff in here
}])
return controller
}
module.exports = {
init: myController
}
Where the above would be called with:
myController.init(angular.module('myApp'))
So, in my unit tests for this controller, I'm trying to do the setup in this manner:
// Test file
const myController = require('../path/to/myController')
const app = angular.module('testModule', [])
myController.init(app)
describe('Controller tests: ', () => {
it('should work', inject(($controller) => {
const testMyCtrl = $controller('MyCtrl', {
$scope: {}
})
assert.equal(true, true)
}
}
But, when I try and test this (using Karma and Mocha), I get:
Argument 'MyCtrl' is not a function, got undefined
Sadly I'm not all that familiar with unit testing in AngularJS, and I'm just struggling to join the dots here. Does anyone have any sage advice?
Okay, sorted this now. I've ended up amending my controller file so it looks like this:
// Controller file
const MyCtrl = ($scope, SomeService) => {
// Stuff here
}
module.exports = myCtrl
Then I instantiate it in my app.js file like this:
import MyCtrl from './path/to/controller'
MyCtrl.$inject = ['$scope', SomeService] // Where 'SomeService' is also imported into this file
const app = angular.module('MyApp', [])
app.controller('MyCtrl', MyCtrl)
Then in my unit tests, I have a 'test-app.js' file that creates the test app and instantiates the services and controllers it needs in the same way (this is probably unnecessary really, it would work just as well using the real app, but it feels nicer to have them separated completely). Then in the file that tests my controller, I have;
// Controller's unit test file
describe('MyCtrl tests: ', () => {
var myCtrl
var $scope
var SomeService
beforeEach(angular.mock.module('testApp')) // testApp is created in test-app.js
beforeEach(angular.mock.inject(($controller, $rootScope, _SomeService_) => {
SomeService = _SomeService_
$scope = $rootScope.$new()
MyCtrl = $controller('MyCtrl', {
$scope: $scope,
SomeService: SomeService
}
}
// tests...
})
And now all of my tests have access to the controller. Hopefully this will be of some use to someone in the future. :)

How does AngularJS dependency injection work with controller testing?

I'm trying to write tests for some AngularJS code, but can't even get a hello world to run. Suppose my code looks like this:
var myApp = angular.module("myApp", [])
myApp.controller("MyCtrl", function ($scope) {
$scope.hello = "world"
})
Then the angular docs here suggest that something like this (using jasmine) should work:
describe("my controller", function () {
it("should say hello", function () {
var $scope
inject(function ($rootScope, $controller) {
$scope = $rootScope.$new()
$controller('MyCtrl', {$scope: $scope})
})
expect($scope.hello).toBe("world")
}
}
Unfortunately, inject does not exist, and there are no hints in the docs as to where to get it. Thus the approach in the docs doesn't work.
Looking slightly farther afield, we find $injector, which can be created by angular.injector. From those docs, it's fairly clear that inject(f) should be $injector.invoke(f). So we stick this at the top of our code and make the change:
$injector = angular.injector(["myApp"])
Unfortunately, this gives the error "Uncaught Error: Unknown provider: $controllerProvider from myApp", which my google-fu seems unable to elucidate.
I had been using this ($injector) previously when I was only testing services, and it works perfectly. It is only when mixed with a controller definition that I get the error. It can handle the controller definition, or the $injector, but not both. To me this suggests some kind of priority conflict or double-initialization, but I can't figure it out.
So what does the "Unknown provider..." error mean, and how do I get my hello world controller test working? If someone can help sort me out, that would be great.
I created for you an skeleton you could use for that concrete controller.
describe('my controller', function() {
var $scope;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
$controller('MyCtrl', { $scope: $scope });
}));
it('should contain world', function() {
expect($scope.hello).toBe('world');
});
});
Before each test, you inject your app module and before each test, you creates a new scope and instantiates your controller. Then you just need to write as much tests as you need.
We create a new scope in every tests to have a fresh state. We don't want to modify some $scope in one test and then have another one failing because you modified your $scope earlier.
Plunker: http://plnkr.co/edit/4hHqdsvnVwyUhiezpdhW?p=preview
Write a comment if you have any question.

Unit testing directives without unnecessary dependencies in module AngularJS

I want to write a test for the directive.
In App
requiresDependencies = ["someModule1","someModule2"];
var app = angular.module('app', requiresDependencies);
In test
describe("Logon Hours Editor", function ()
{
var compile, rootScope;
var element;
beforeEach(module('app'));
beforeEach(inject(['$compile', '$rootScope', function ($compile, $rootScope)
{
compile = $compile;
rootScope = $rootScope;
element = $compile('some html')($rootScope);
} ]
)); ....
My directive is relative to the main unit, and I do not want to connect other test modules (someModule1,someModule2) described in "requiresDependencies" ,because later their numbers and names can be changed.
How do I connect only without his dependencies?
You have to mock them. For example, you can create a module called mocks with a d with the services or directives you want to mock. You then load it with beforeEach(module('mocks')); and your calls from the test suite to whatever service you have in mocks will be using the dummy implementation.
You can put this mock module in your test folder, like /test/lib/my-mocks.js (next to angular-mocks.js, if you're using angular-seed). Lastly, you include it in karma.conf.js:
files = [
'app/lib/jquery/jquery-1.9.1.js',
JASMINE,
JASMINE_ADAPTER,
'test/lib/jasmine-jquery.js',
'app/lib/angular/angular.js',
...
'test/lib/myproject/my-mocks.js',
...
];
Another approach is using jasmine spies. For example:
spyOn($location, 'absUrl').andCallFake(function (p) {
return 'http://localhost/app/index.html#/this/is/my/route';
});

Mocking collaborators in Angular controller and service Jasmine tests

I have been writing some Jasmine unit tests in Angular. In the first example I'm testing a controller.
myApp.controller('MyCtrl', function($scope, Config){
...
});
I have a configuration service (Config) that keeps configuration from the database and is injected into my controller. As this is a unit test, I want to mock out that configuration service altogether, rather than allowing execution to pass through it and using $httpBackend. Examples I found taught me about a $controller function I can use like this, in order to get an instance of my controller with my mocks injected in place of the usual collaborator:
beforeEach(inject(function($controller, $rootScope){
var scope = $rootScope.$new();
var configMock = {
theOnlyPropertyMyControllerNeeds: 'value'
};
ctrl = $controller('MyCtrl', {
$scope:scope,
Config: configMock
});
}));
But I also have other services that use the Config service. To help unit test them, I assumed there would be a similar $service function I could use to instantiate a service with whatever mocks I want to provide. There isn't. I tried $injector.get, but it doesn't seem to let me pass in my mocks. After searching for a while, the best I could come up with in order to instantiate a service in isolation (avoid instantiating its collaborators) is this:
beforeEach(function() {
mockConfig = {
thePropertyMyServiceUses: 'value'
};
module(function($provide) {
$provide.value('Config', mockConfig);
});
inject(function($injector) {
myService = $injector.get('MyService');
});
});
Is this the right way? It seems to be overriding the entire application's definition of the Config service, which seems maybe like overkill.
Is it the only way? Why is there no $service helper method?
For unit testing, it is common that you override a service for the sake of testing. However, you can use $provide to override an existing service instead of using inject, as long as you load the application before hand.
Assuming that you created Config using something like:
angular.moduel('...', [...]).factory('Config', function (...) {...});
If so, try this:
...
beforeEach(module("<Name of you App>"));
beforeEach(
module(function ($provide) {
$provide.factory('Config', function (...) {...});
});
);
...
After that, when you initialise your controller, it will get the mocked Config.

AngularJS - Error: Unknown provider: searchResultsProvider

EDIT: I have managed to get my unit tests running - I moved the code containing the services to a different file and a different module, made this new module a requirement for fooBar module, and then before each "it" block is called, introduced the code beforeEach(module(<new_service_module_name)). However, my application still won't run. No errors in console either. This is the only issue that remains - that when I use global scope for controllers definition, the application works, but when I use angular.module.controller - it does not.
I have a file app.js that contains the following:
'use strict';
var app = angular.module('fooBar', []);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/', {
templateUrl: 'partials/form-view.html',
controller: FormViewCtrl
}).
when('/resultDisplay', {
templateUrl: 'partials/table-view.html',
controller: TableViewCtrl
}).
otherwise({redirectTo: '/'});
}]);
app.service('searchResults', function() {
var results = {};
return {
getResults: function() {
return results;
},
setResults: function(resultData) {
results = resultData;
}
};
});
I have another file controllers.js that contains the following:
'use strict';
var app = angular.module('fooBar', []);
app.controller('FormViewCtrl', ['$scope', '$location', '$http', 'searchResults',
function ($scope, $location, $http, searchResults) {
//Controller code
}]);
searchResults is a service that I created that simply has getter and setter methods. The controller above uses the setter method, hence the service is injected into it.
As a result, my application just does not run! If I change the controller code to be global like this:
function ($scope, $location, $http, searchResults) {
//Controller code
}
then the application works!
Also, if I use the global scope, then the following unit test case works:
'use strict';
/*jasmine specs for controllers go here*/
describe('Foo Bar', function() {
describe('FormViewCtrl', function() {
var scope, ctrl;
beforeEach(module('fooBar'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('FormViewCtrl', {$scope: scope});
}));
}
//"it" blocks
}
If I revert to the module scope, I get the error -
Error: Unknown provider: searchResultsProvider <- searchResults
Thus, by using global scope my application and unit tests run but by using app.controller, they seem to break.
Another point that I have noted is that if I include the controller code in app.js instead of controllers.js, then the application and unit tests start working again. But I cannot include them in the same file - how do I get this to run in the angular scope without breaking the application and unit tests?
You don't need to go that route. You can use the modular approach, but the issue is with your second parameter.
In your app.js you have this:
var app = angular.module('fooBar', []);
Then in your controller, you have this:
var app = angular.module('fooBar', []);
What you're doing there is defining the module twice. If you're simply trying to attach to the app module, you cannot pass in the second parameter (the empty array: []), as this creates a brand new module, overwriting the first.
Here is how I do it (based on this article for architecting large AngularJS apps.
app.js:
angular.module('fooBar',['fooBar.controllers', 'fooBar.services']);
angular.module('fooBar.controllers',[]);
angular.module('fooBar.services', []);
...etc
controllers.js
angular.module('foobar.controllers') // notice the lack of second parameter
.controller('FormViewCtrl', function($scope) {
//controller stuffs
});
Or, for very large projects, the recommendation is NOT to group your top-level modules by type (directives, filters, services, controllers), but instead by features (including all of your partials... the reason for this is total modularity - you can create a new module, with the same name, new partials & code, drop it in to your project as a replacement, and it will simiply work), e.g.
app.js
angular.module('fooBar',['fooBar.formView', 'fooBar.otherView']);
angular.module('fooBar.formView',[]);
angular.module('fooBar.otherView', []);
...etc
and then in a formView folder hanging off web root, you THEN separate out your files based on type, such as:
formView.directives
formView.controllers
formView.services
formView.filters
And then, in each of those files, you open with:
angular.module('formView')
.controller('formViewCtrl', function($scope) {
angular.module('formView')
.factory('Service', function() {
etc etc
HTH
Ok - I finally figured it out. Basically, if you wish to use the module scope and not the global scope, then we need to do the following (if you have a setup like app.js and controllers.js):
In app.js, define the module scope:
var myApp = angular.module(<module_name>, [<dependencies>]);
In controllers.js, do not define myApp again - instead, use it directly like:
myApp.controller(..);
That did the trick - my application and unit tests are now working correctly!
It is best practice to have only one global variable, your app and attach all the needed module functionality to that so your app is initiated with
var app = angular.module('app',[ /* Dependencies */ ]);
in your controller.js you have initiated it again into a new variable, losing all the services and config you had attached to it before, only initiate your app variable once, doing it again is making you lose the service you attached to it
and then to add a service (Factory version)
app.factory('NewLogic',[ /* Dependencies */ , function( /* Dependencies */ ) {
return {
function1: function(){
/* function1 code */
}
}
}]);
for a controller
app.controller('NewController',[ '$scope' /* Dependencies */ , function( $scope /* Dependencies */ ) {
$scope.function1 = function(){
/* function1 code */
};
}
}]);
and for directives and config is similar too where you create your one app module and attach all the needed controllers, directives and services to it but all contained within the parent app module variable.
I have read time and time again that for javascript it is best practice to only ever have one global variable so angularjs architecture really fills that requirement nicely,
Oh and the array wrapper for dependencies is not actually needed but will create a mess of global variables and break app completely if you want to minify your JS so good idea to always stick to the best practice and not do work arounds to get thing to work
In my case, I've defined a new provider, say, xyz
angular.module('test')
.provider('xyz', function () {
....
});
When you were to config the above provider, you've inject it with 'Provider' string appended.
Ex:
angular.module('App', ['test'])
.config(function (xyzProvider) {
// do something with xyzProvider....
});
If you inject the above provider without the 'Provider' string, you'll get the similar error in OP.

Resources