Unit-testing with Karma-Jasmine - angularjs

I'm using angularJS and i understand how to test my $scope objects with karma-jasmine but i'm having difficulties testing regular functions and variables inside my controller file
//controller.js
angular.module('myApp').controller('mainCtrl', function ($scope) {
$scope.name = "bob";
var aNumber = 34;
function myFunction(string){
return string;
}
});
what i would like to do is to test to see if expect(aNumber).toBe(34);
// test.js
describe('Controller: mainCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
var mainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
mainCtrl = $controller('mainCtrl', {
$scope: scope
});
}));
// understand this
it('should expect scope.name to be bob', function(){
expect(scope.name).toBe('bob');
});
// having difficulties testing this
it('should expect aNumber to be 34', function(){
expect(aNumber).toBe(34);
});
// having difficulties testing this
it('should to return a string', function(){
var mystring = myFunction('this is a string');
expect(mystring).toBe('this is a string');
});
});

It looks you are trying to test private variables declared in angular controller. The variables which are not exposed through the $scope cannot be tested, as they are hidden, and are visible only in a function scope inside the controller. More on private members and information hiding in javascript you can find here
The way how you should approach private fields in tests is by testing them through exposed api. If the variable is not used in any exposed publicly method it means that it's not used so it doesn't make sense to keep it and test it.

Related

Angular/Mocha/Chai - initialize controller for unit testing?

We're just getting started with unit testing in our Angular app and we are using a karma/mocha/chai framework for unit testing. I carried out some basic unit tests on various services and factories we have defined and it the unit testing works great. However now we want to test some controller and evaluate the scope vars that the controllers are modifying.
Here is an example of one such controller:
angular.module('App').controller('myCtrl', ['$scope', 'APIProxy',
function ($scope, APIProxy) {
$scope.caseCounts = {caseCount1: 0, caseCount2: 0};
$scope.applyCounts = function () {
$scope.caseCounts.caseCount1 = {...some case count logic...}
$scope.caseCounts.caseCount2 = {...some case count logic...}
};
APIProxy.getAll().then(function (data) {
{...do a bunch of stuff...}
$scope.data = data;
$scope.applyCounts();
});
}]
);
Now, when I unit test I would like to start off with just a simple 'does the $scope.caseCounts have values > 0, then I'll build from there. However it is not obvious how to make the controller cause the APIProxy service run and how to handle the eventual return of data. We've tried $scope.getStatus(), and $scope.apply() and a few other things but I feel like we are way off the mark and we are fundamentally missing something about how to go about this.
Currently our controller tester looks like:
describe("myCtrl unit tests",function(){
beforeEach(module('App'));
var ctrl, APIProxy;
beforeEach(inject(function ($rootScope, $controller, _APIProxy_)
{
$scope = $rootScope.$new();
APIProxy = _APIProxy_;
ctrl = $controller('myCtrl', {$scope: $scope, APIProxy: APIProxy});
}));
it('Loads data correctly', function() {
expect(ctrl).to.not.be.undefined;
//??? what else do we do here to fire the getAll function in controller?
});
});
It's usually better to test the service and the controller separately.
To test the service, you can use $httpBackend to mock the XHR requests:
https://docs.angularjs.org/api/ngMock/service/$httpBackend
To test the controller, you can simply provide mocked values instead of the actual service when you initalize the controller
APIProxy = {'mocked':'data'};
ctrl = $controller('myCtrl', {$scope: $scope, APIProxy: APIProxy});
Or more generally, to mock any provider of your module:
module(function($provide) {
$provide.constant('ab','cd');
$provide.value('ef', 'gh');
$provide.service('myService', function() { });
});
Which will override the 'myService' referenced as dependencies in your controller (if there is one). If you need it directly, you can then inject it too:
var myService;
beforeEach(inject(function (_myService_) {
myService = _myService_;
}));
If you need APIProxy to return a promise, you can mock it too with
https://docs.angularjs.org/api/ng/service/$q
and resolve, e.g.:
var deferred = $q.defer();
deferred.resolve({'mocked':'data'});
return deferred.promise;
If you do want to test them together, you can do a spy on the API function you call and have the spy return a resolved promise.

How do I spy on a controller method using Jasmine?

I'm using the "controller as" syntax to create my controller. I have a private initialization function that calls a function to load the default data.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var mc = this;
mc.dataLoaded = false;
function init() {
mc.loadData();
}
mc.loadData = function(){
mc.dataLoaded = true;
}
init();
});
In my test I'm creating a spy to check whether the loadData function has been called. Although I can verify that the function has been called by testing for the mc.dataLoaded flag, my spy doesn't seem to record the function being called. How can I get the spy to correctly record the function call?
describe('Testing a Hello World controller', function() {
var $scope = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
spyOn($scope.mc, 'loadData').and.callThrough();
}));
it('should call load data', function() {
expect($scope.mc.loadData).toHaveBeenCalled();
//expect($scope.mc.dataLoaded).toEqual(false);
});
});
Plunker link
This sequence of lines:
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
spyOn($scope.mc, 'loadData').and.callThrough();
Means that the Jasmine spy is created after the controller has already been instantiated by $controller. Before the spy is created, the init function has already executed.
You can't switch the lines around either, because MainCtrl needs to exist before you can spy on a method on it.
If the init function calls another service, then spy on that service's method and assert that the service is called correctly. If MainCtrl is just doing something internally, then test the result of that, for example, by asserting that controller's data/properties are updated. It may not even be worth testing if it's trivial enough.
Also, since you're using the controller as syntax, you can reference the controller through the return value of calling $controller, rather than accessing the scope directly:
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
ctrl.loadData === $scope.mc.loadData; // true
I found a solution that allowed me to avoid changing my controller. I included a $state mock service in the test suite's beforeEach method, and gave it a reload mock method:
beforeEach(inject(function ($controller, $rootScope) {
stateMock = {
reload: function() {
myCtrl = $controller('MyCtrl');
}
};
...
Then within the jasmine tests, I can simply call stateMock.reload() to re-initialize my controller while preserving my spies I declared in another beforeEach block.

AngularJs: Test service with Jasmine

I did this controller
app.controller('controller',['$scope','httpServices',function($scope,httpServices){
$scope.items= undefined;
httpServices.getItems( function(items){
$scope.items= items;
});
}]);
and I wrote this test
describe('controller', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('controller', {
'$scope': scope
});
}));
it('defined', function () {
expect(scope.items).toBeUndefined();
})
});
How I can test the scope.items after to have called the service?
I assume that your service httpServices is making some http requests. Therefore you should use the mock-backend service in order to test your controller.
Something like this, pay attention to the comments that I've made inside the code:
describe('Your specs', function() {
var $scope,
$controller,
$httpBackend;
// Load the services's module
beforeEach(module('yourApp'));
beforeEach(inject(function(_$controller_, $rootScope, _$httpBackend_) {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$controller = _$controller_;
//THIS LINE IS VERY IMPORTANT, HERE YOU HAVE TO MOCK THE RESPONSE FROM THE BACKEND
$httpBackend.when('GET', 'http://WHATEVER.COM/API/SOMETHING/').respond({});
var createController = function(){
$controller('controller', {$scope: $scope});
}
}));
describe('Your controller', function() {
it('items should be undefined', function() {
createController();
expect(scope.items).toBeUndefined();
});
it('items should exist after getting the response from the server', function () {
//THIS LINE IS ALSO VERY IMPORTANT, IT EMULATES THE RESPONSE FROM THE SERVER
$httpBackend.flush();
expect(scope.items).toBeDefined();
});
});
});
The question title states this is to test a service, but the code of the question looks like an attempt is being made to test the controller. This answer describes how to test the controller.
If you're testing the controller that calls httpServices.getItems, then you need to mock it/stub getItems in order to
Control it on the test
Not assume any behaviour of the real httpServices.getItems. After all, you're testing the controller, and not the service.
A way to do this is in a beforeEach block (called before the controller is created) provide a fake implementation of getItems that just saves the callback passed to it.
var callback;
beforeEach(inject(function(httpServices) {
callback = null;
spyOn(httpServices, 'getItems').and.callFake(function(_callback_) {
callback = _callback_;
});
});
In the test you can then call this callback, passing in some fake data, and test that this has been set properly on the scope.
it('saves the items passed to the callback on the scope', function () {
var testItems = {};
callback(testItems);
expect($scope.items).toBe(testItems);
});
This can be seen working at http://plnkr.co/edit/Z7N6pZjCS9ojs9PZFD04?p=preview
If you do want to test httpServices.getItems itself, then separate tests are the place for that. Assuming getItems calls $http, then you are most likely to need to use $httpBackend to handle mock responses. Most likely, these tests would not instantiate any controller, and I suspect not need to do anything on any scope.

Trigger a function in a controller with Karma

In an angular controller I have a function on my scope that looks like this:
$scope.myFunction = function(val) {
$scope.myVar = val;
$scope.mySettings.myPath = $scope.myBase + $scope.myVar;
}
In my karma unit test I want to trigger that function on the controller using a value i supply ... then check $scope.mySettings.myPath to make sure it is what i think it should be.
Im trying this in karma:
describe("Unit Testing: Controller Testing" , function() {
describe('myController' , function () {
var scope,ctrl;
beforeEach(module('myApp.controllers'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('myController', {
$scope: scope
});
}));
it('should have a working changeVideo function' , function(){
scope.changeVideo(scope.myVar[6]);
expect(scope.mySettings.myPath).to.equal('Expected String');
});
});
});
The error i see is that it cannot call equal method of undefined. Which is odd because scope.mySettings.myPath is actually predefined in the controller so it should never be undefined.
To give some background I do have access to the controller and the scope from karma. This one is stumping me though.
The solution to this was using toEqual() instead of to.equal()

Testing named function within a Controller

A newbie Jasmine/Angular question.
I have a named function within a controller like so:
.controller( 'DummyCtrl', function DummyCtrl($scope){
var doSomething = function() {
return "blah";
};
})
I need to test this function, and am trying to by calling the following Jasmine spec:
describe ('myApp', function(){
var $scope, $controller;
var DummyCtrl;
beforeEach(module('myApp'));
describe('controllers', function(){
beforeEach(inject(function ($controller, $rootScope){
$scope = $rootScope.$new();
DummyCtrl = $controller('DummyCtrl', {$scope: $scope});
}));
describe( 'DummyCtrl', function(){
var blah;
beforeEach(function(){
blah = DummyCtrl.doSomething();
});
it('should do something', function(){
expect(blah).toContain("blah");
});
});
});
});
Instead of things working out, I result in the following error: TypeError: Object #<DummyCtrl> has no method 'doSomething'. I'm assuming this is something super simple that I'm not understanding.
The function DummyCtrl you are providing for the controller registration will be used by Angular as a constructor. If you need the controller instance to expose the function doSomething without attaching it to the $scope, you should attach it to this.
Try changing
var something = function(...
to
this.something = function(...
and your test should work.
You can see this approach here: http://jsfiddle.net/yianisn/8P9Mv/. Also have a look at this SO question: How to write testable controllers with private methods in AngularJs?
In a sense, using functions like that is private, and cannot be accessed from outside the function. Take a look at this link: http://javascript.crockford.com/private.html
Essentially what is said is that have a function/object in javascript, anything with a this. prefix is public, and anything with a var prefix is private.
For Angular, you can definitely have private variables and functions, if not just to lessen the memory usage of the $scope variable. Private functions should be called by your $scope objects to get values to be displayed/used by the user. Try changing it to this:
.controller( 'DummyCtrl', function DummyCtrl($scope){
var doSomething = function() {
return "blah";
};
$scope.something=doSomething();
})
And then testing the private function with:
describe( 'DummyCtrl', function(){
var scope = {},
ctrl = new DummyCtrl(scope);
it('should do something', function(){
expect(scope.something).toMatch('blah');
});
});

Resources