Can you please suggest some good unit test for the following Ctrl?
I want to make sure the redirect works correctly
Is there anyway I could use Jasmine Spies here / any benefits?
Is it possible to test how the controller will behave on forced redirect (redirect to a page with indention of being redirected)?
angular.module('app')
.controller('MainCtrl', ['$scope', 'API', '$location', function ($scope, API, $location) {
// redirect back to login page
if( ! API.token ) $location.path('/');
}]);
A unit test should only be concerned about the component that it's testing. You shouldn't need to test what $location actually does but rather that you are calling the method when required. Also you don't care about what the API service does just that when token is falsy the controller calls the location method.
I would:
Mock the service API.
Spy the $location.path method.
Set API.token to true.
Check that $location.path has not been called.
Set API.token to false
Check that $location.path has been called with the parameter "/".
Something like this:
describe('Controller: MainCtrl', function() {
// Define this test's local variables
var scope,
$location,
MainCtrl;
// Load the controller's module
beforeEach(angular.mock.module('app'));
/* jshint camelcase:false */
// Initialize the controller and scope
beforeEach(angular.mock.inject(function($controller, $rootScope, _$location_) {
scope = $rootScope.$new();
$location = _$location_;
spyOn($location, 'path');
MainCtrl = $controller('MainCtrl', {
$scope: scope,
API: {token: false},
$location: $location
});
}));
it('should exist', function() {
expect(MainCtrl).toBeTruthy();
});
describe('when created', function() {
it('should call $location accordingly', function () {
expect($location.path).toHaveBeenCalledWith('/');
});
});
});
Related
I'm testing a directive ('planListing') that has a dependency on a service called 'planListingService'. This service has a dependency to another service called 'ajax' (don't shoot the messenger for the bad names).
I'm able to compile the directive, load its scope and get the controller WITH A CAVEAT. As of now I am being forced to mock both services 'planListingService' and 'ajax' otherwise I will get an error like this:
Error: [$injector:unpr] Unknown provider: ajaxProvider <- ajax <- planListingService
http://errors.angularjs.org/1.3.20/$injector/unpr?p0=ajaxProvider%20%3C-%20ajax%20%3C-%20planListingService
I thought that because I was mocking up the 'planListingService' that I wouldn't have to actually bother with any implementation nor any dependencies of this service. Am I expecting too much?
Here is the code in a nutshell:
planListing.js
angular.module('myApp')
.directive('planListing', planListing)
.controller('planListingCtrl', PlanListingCtrl);
function planListing() {
var varDirective = {
restrict: 'E',
controller: PlanListingCtrl,
controllerAs: 'vm',
templateUrl: "scripts/directives/planListing/planListing.html";
}
};
return varDirective;
}
PlanListingCtrl.$inject = ['planListingService'];
function PlanListingCtrl(planListingService) {
...
}
planListingService.js
angular.module('myApp')
.factory('planListingService', planListingService);
planListingService.$inject = ['$q', 'ajax'];
function planListingService($q, ajax) {
...
}
ajax.js
angular.module('myApp')
.factory('ajax', ['backend', '$browser', 'settings', '$http', '$log',
function (backend, $browser, settings, $http, $log) {
...
planListing.spec.js
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
beforeEach(module(function ($provide){
// This seems to have no effect at all, why?
$provide.service('planListingService', function () {
this.getAllPricePlans=function(){};
});
// I don't get the error if I uncomment this:
// $provide.service('ajax', function ($q) {
// this.getAllPricePlans=function(){};
// });
}));
beforeEach(function() {
module('myApp');
module('my.templates');
});
beforeEach(angular.mock.inject(function (_$compile_,_$rootScope_,_$controller_){
$compile=_$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
el = angular.element('<plan-listing></plan-listing>');
scope = $rootScope.$new();
$compile(el)(scope);
scope.$digest();
ctrl = el.controller('planListing');
scope = el.isolateScope() || el.scope();
vm = scope.vm;
}));
describe('testing compilation / linking', function (){
it('should have found directive and compiled template', function () {
expect(el).toBeDefined();
expect(el.html()).not.toEqual('');
expect(el.html()).toContain("plan-listing-section");
});
});
it('should have a defined controller',function(){
expect(ctrl).toBeDefined();
});
it('should have a defined scope',function(){
expect(ctrl).toBeDefined();
});
});
So why is that I need to mock up the 'ajax' service even though I am mocking up 'planListingService' which is the one calling the 'ajax' service?
Thanks!
I have been there... feels like bad start But i think your directive is depend on the service and you need to inject it in order to directive can work with this, Just by calling directive it doesn't mean that it's going to inject it in your test. It will look for it and if it's not injected it will give you error
you could do so before testing your directive
beforeEach(inject(function ($injector) {
yourService = $injector.get('yourService');
})
For documentation purposes, here is the answer (thanks #estus for noticing this):
Indeed the problem was related to the incorrect initialization of my modules. Instead of this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
...
I should've done this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
beforeEach(module('myApp'));
beforeEach(module('my.templates'));
...
After that things started working again as expected.
My controller method looks like this:
angular.module(_appName_)
.controller('myController', function ($scope, $rootScope) {
$rootScope.$broadcast('myObj', false);
......some code here.......
});
Jasmine test for testing call made to $rootScope.$broadcast looks like this:
describe("myController",function(){
var scope,rootScope;
beforeEach(angular.mock.inject(function($rootScope) {
scope = $rootScope.$new();
rootScope = $rootScope;
}));
describe('myController', function() {
it('rootScope broadcast called for myObj with false value', inject(function($controller, $rootScope) {
var requestObj = '{"key":"1234567890"}';
rootScope.requestObject = requestObj;
$controller('myController', {
$scope: scope,
$rootScope: rootScope
});
spyOn($rootScope, '$broadcast').and.callThrough();
expect($rootScope.$broadcast).toHaveBeenCalled();
}));
});
});
It always gives me the following error:
Expected spy $broadcast to have been called.
at Object.
When i try to put a breakpoint on the line where there is a call to broadcast in the controller method, it does hit the breakpoint while debugging. So the actual call is being made but the test doesn't recognize it somehow.
Can someone please let me know what am I missing here ?
I think you forgot to include your module in beforeEach function.
And then make sure you mock your spyOn($rootScope, '$broadcast') before you initialize your controller
$controller('myController', {
$scope: scope,
$rootScope: rootScope
});
Here is a plunker. :)
I am trying to integrate Karma and Jasmine in to my project.
I have started off with a very basic test to ensure my controller is defined and a $scope variable equals a string - which pass as expected.
My controller, also calls a service which performed a $http.get, when running my test, without any mention of a service, i get the error:
Error: Unexpected request: GET /my/endpoint/
No more request expected
Controller:
define(['module'], function (module) {
'use strict';
var MyController = function ($scope, MyService) {
$scope.testScope = 'karma is working!';
MyService.getData().then(function (data) {
$scope.result = data.hour
});
};
module.exports = ['$scope', 'MyService', MyController ];
});
Test:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_) {
scope = _$rootScope_.$new();
controller = _$controller_('MyController', {$scope: scope});
scope.$apply();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
it('should have testScope scope equaling *karma is working*', function() {
expect(scope.testScope ).toEqual('karma is working!');
});
});
});
Are the above errors expected?
UPDATE from response below:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope, $httpBackend, myService;
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _myService_) {
scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.expectGET("/my/endpoint");
controller = _$controller_('MyController', {$scope: scope});
scope.$apply();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
it('should have testScope scope equaling *karma is working*', function() {
expect(scope.testScope ).toEqual('karma is working!');
});
});
});
Using Angular Mocks you will always get an error if there is an unexpected or incorrect http request attempted -- even for templates. In your case there are two ways to handle this for testing:
use $httpBackend
$httpBackend was designed for testing http requests without actually hitting the wire. In your test, simply add
$httpBackend.expectGET("/my/endpoint");
before you initialize the controller.
Mock the service
The service itself is making the http request, so you can mock the service instead. Services will be injected automatically as usual, but you can explicitly injection whatever you want:
controller = _$controller_('MyController', {$scope: scope,
MyService: {getData: () => ({then: () => {}}) });
This injects an object that has a getData function which returns an object with .then function. Of course this doesn't come close to implementing what you are trying to do, but it is another way to perform the test.
Both of the above approaches are valid. It depends on what you are testing and what you are trying to accomplish with the testing.
I'm using Karma, Mocha, Sinon and Chai for my Angular unit tests and I'm trying to figure out how to mock a redirect I'm doing in my controller with $location.
My controller does the following redirect:
$location.path('home');
I want to try and mock that redirect using spies, this is what I'm currently doing:
describe('Auth Controller', function() {
var controller;
var $location;
beforeEach(function() {
bard.appModule('app.auth');
bard.inject('$controller', '$rootScope', '$location');
});
beforeEach(function() {
$location = {
path: sinon.spy().returned('Fake location')
};
controller = $controller('authCtrl', { $scope: $rootScope, $location: $location });
});
it('should take you to the metrics page on successful login', function() {
expect($location.path).to.have.been.calledWith("Fake location");
});
});
I'm getting the following error:
TypeError: false is not a spy or a call to a spy!
I'm not sure how to go about mocking this correctly or if I'm even going about this in the right way.
Any help for unit testing experts are appreciated. Thanks in advance!
You can use Spies for testing location.path like this (see f.e. here: Spy on a service method call using jasmine Spies):
var location, objectUnderTest;
beforeEach(inject(function($location){
location = $location;
}));
function YourCtrlMaker() {
objectUnderTest = $controller('YourCtrl', {
$scope: $scope,
$location: location,
$routeParams: $routeParams,
})
}
it('should test location.path', function(){
spyOn(location, 'path');
YourCtrlMaker();
$scope.$root.$digest();
expect(location.path).toHaveBeenCalledWith('example.com/objects/');
});
I am testing my angularjs application with Jasmine and Karma.
My test looks like this:
describe('Login test', function() {
// Mock our module in our tests
beforeEach(module('Project'));
var ctrl, scope;
// inject the $controller and $rootScope services
// in the beforeEach block
beforeEach(inject(function($controller, $rootScope) {
// Create a new scope that's a child of the $rootScope
scope = $rootScope.$new();
// Create the controller
ctrl = $controller('loginController', {
$scope: scope
});
}));
it('should get login success',function() {
scope.loginClick('user', 'pass');
});
});
I have a login controller with the loginClick function, and inside this function i have another function which is making a POST request. The problem is that the inner function is never executed, i try to console.log() to see if the function is called but with no success.
My function looks like this:
app.controller('loginController', ['$scope', '$http', '$route', function ($scope, $http, $route) {
console.log('controller call'); // yes it works
...
$scope.loginClick = function (username, password) {
console.log('controller call'); // yes it works
handler.reqPOST('/login', userData, function (result) {
console.log('login request'); // no output is sent to the console
});
};
}]);
The handler object is include in the karma configuration file at start-up.
First of all, unless you have very good reason, $http is the way to go in angularJS to call the back-end, it also makes it more testable.
In any case you should mock the post call, in a unit-test you don't want to rely on the back-end
In your case, you could use a spy (http://jasmine.github.io/2.0/introduction.html#section-Spies):
describe('Login test', function(){
beforeEach(module('Project'));
var ctrl, scope;
beforeEach(inject(function($injector) {
var $controller = $injector.get('$controller');
var $rootScope = $injector.get('$rootScope');
scope = $rootScope.$new();
ctrl = $controller('loginController', {
$scope: scope,
});
}));
it('test login', function () {
spyOn(handler, 'reqPOST');
scope.loginClick('user', 'pass');
expect(handler.reqPOST).toHaveBeenCalledWith('user', 'pass');
});
});