How to test Angular 1.6 component with injected service? - angularjs

I want to test my Angular component which is syntactically based on John Papa's styleguide:
'use strict';
angular.module('MyModule')
.component('MyCmpnt', MyCmpnt())
.controller('MyCtrl', MyCtrl);
function MyCmpnt() {
return {
restrict: 'E',
templateUrl: 'myPath/myTemplate.html',
bindings: {
foo: '=',
bar: '<'
},
controller: 'MyCtrl',
controllerAs: 'vm'
};
}
MyCtrl.$inject = ['MyService'];
function MyCtrl (MyService) {
// controller logic
}
As you can see I want to inject MyService into the controller and spy in a function on that very service.
My test code:
'use strict';
describe('component: MyCmpnt', function () {
var $componentController,
MyService;
beforeEach(module('MyModule'));
beforeEach(module(function ($provide) {
$provide.value('MyService', MyService);
spyOn(MyService, 'serviceFunc').and.callThrough();
}));
beforeEach(inject(function (_$componentController_) {
$componentController = _$componentController_;
}));
it('should initiate the component and define bindings', function () {
var bindings = {
foo: 'baz',
bar: []
};
var ctrl = $componentController('MyCmpnt', null, bindings);
expect(ctrl.foo).toBeDefined();
});
});
However, this setup lets me run into the following error:
TypeError: undefined is not a constructor (evaluating '$componentController('MyModule', null, bindings)')

The code above has $componentController('MyModule'..., and there is no MyModule component.
MyService variable is undefined when spyOn(MyService... is called. This will throw an error prevent the application from being bootstrapped correctly.
If testing rig uses PhantomJS, this may lead to error suppression in beforeEach blocks, for correct error reporting Chrome Karma launcher is recommended.
If the problem is that MyService is undefined at the point where mocked service is defined, it can be defined in-place as a stub:
beforeEach(module(function ($provide) {
$provide.value('MyService', {
serviceFunc: jasmine.createSpy().and.callThrough()
});
}));

Related

jasmine mocking service inside service

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.

How to test an angular app with class inheritance?

I can test normal controllers fine. I cannot test controllers that inherit a base controller.
This is how we've been defining subclassed-controllers:
app.NavController = app.BaseController.extend({
...
});
This is the base:
app.BaseController = Class.extend({
$scope: null,
init: function($scope) {
this.$scope = $scope;
this.defineListeners();
this.defineScope();
},
defineListeners: function() {
// this.$scope.$on('$destroy',this.destroy.bind(this));
},
...
});
app.controller('BaseController', ['$scope', function($scope) {
return new app.BaseController($scope);
}]);
However, running Karma produces:
TypeError: 'undefined' is not an object (evaluating 'app.BaseController.extend')
Is there a different way of doing this? I've removed IIFE wrappers for testing. Class.js is included in my Karmaconfig. I'm using John Resig's Class inheritance.
by adding this to instantiate the sub-classes prototype, we can get access to base class by simply injecting $controller as a dependency.
app.controller('SubController', ['$scope','$location','$controller',
function($scope, $location, $controller) {
var controller = {
...
controller.prototype = $controller('BaseController', {
$scope: $scope
});
controller.init($location);
return controller;

Using a service in a unit test, angular

I have this controller:
angular.module('clientApp')
.controller('MainCtrl', function ($scope, projects) {
$scope.projects = projects;
});
projects is a resolve from a database. It works in the view.
This is my service:
angular.module('clientApp.services', ['ngResource'])
.factory('Projects', function($resource){
return $resource('/api/project/:prj_id', {'prj_id':'#prj_id'});
})
.factory('MultiProjectsLoader',['Projects', '$q', '$stateParams',
function(Projects, $q) {
return function() {
var delay = $q.defer();
Projects.query(function(projects) {
delay.resolve(projects);
}, function() {
delay.reject('Unable to fetch sizes');
});
return delay.promise;
};
}
]);
And this is my app.js
$stateProvider
.state('home', {
url: '/',
templateUrl: 'views/home.html',
resolve:{
projects: ['MultiProjectsLoader', function(MultiProjectsLoader){
return new MultiProjectsLoader();
}]
},
controller: 'MainCtrl'
});
Trying to write a test for this:
'use strict';
describe('Controller: MainCtrl', function () {
// load the controller's module
beforeEach(module('clientApp'));
beforeEach(function () {
angular.module('clientApp.services');
});
var MainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
it('should attach a list of projects to the scope', function () {
expect(scope.projects.length).toBeGreaterThan(1);
});
});
I get:
Error: [$injector:unpr] Unknown provider: projectsProvider <- projects
I guess I need to include the service somehow beforeEach(..). But I can't get it working. Any ideas?
You can inject the service a couple ways but my recommended way is to mock the service.
describe('Controller: MainCtrl', function () {
var
projectServiceMock = {
getData: function() {}
},
DATA_FROM_SERVER = {}
;
// load the controller's module
beforeEach(module('clientApp'));
beforeEach(function () {
angular.module('clientApp.services');
//angular.module('project'); // But dont use this method as you will be testing the service in the controller:
});
var MainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
spyOn(projectServiceMock, 'getData').andReturn(DATA_FROM_SERVER);
MainCtrl = $controller('MainCtrl', {
$scope: scope,
project: projectServiceMock
});
}));
it('should attach a list of projects to the scope', function () {
expect(projectServiceMock.getData).toHaveBeenCalledWith(DATA_FROM_SERVER);
expect(scope.projects.length).toBeGreaterThan(1);
});
});
Your service should expose a method for returning the data it has gotten from the server not just straight data through project.
For example:
project.getData();

How to mock the Controller of an AngularJS Directive

Given a directive that has an external controller:
.directive('d1', function () {
return {
controller: 'd1controller',
restrict: 'E',
link: function ($scope, $element, $attributes, $controller) {
$controller.doStuff();
}
};
});
How do I mock the d1controller controller in the d1 directive's unit tests?
My attempts:
I tried with $provide as when mocking a service:
beforeEach(module('app', function ($provide) {
ctrlMock = jasmine.createSpyObj('ctrlMock', ['doStuff']);
$provide.value('d1controller', ctrlMock );
}));
And I also tried with $controllerProvider
beforeEach(module('app', function ($controllerProvider) {
ctrlMock = jasmine.createSpyObj('ctrlMock', ['doStuff']);
$controllerProvider.register('d1controller', ctrlMock);
}));
I'm the OP. It turns out that using $controllerProvider works. You have to pass it a constructor; not an instance.
beforeEach(module('app', function ($controllerProvider) {
$controllerProvider.register('d1controller', function Mock(){
this.doStuff = function(){};
});
}));
I have had decent luck creating a mock module, that contains all of my mocked services/controllers and then including that mock module after my app module. It will essentially override all of the services/controllers you have in your mock module.
Create your mock module, and your controller to mock in that module.
appMock = angular.module('appMock', []);
appMock.controller('ControllerToMockCtrl', ['$scope', function($scope) {
$scope.greeting = 'Hola!';
}]);
In your test.
beforeEach(function(){
module('app');
module('appMock');
});
This blog is a good example of this strategy. http://southdesign.de/blog/mock-angular-js-modules-for-test-di.html#using-the-mock

Unit testing controller which uses $state.transitionTo

within a controller i have a function which uses $state.transitionTo to "redirect" to another state.
now i am stuck in testing this function, i get always the error Error: No such state 'state-two'. how can i test this? it its totally clear to me that the controller does not know anything about the other states, but how can i mock this state?
some code:
angular.module( 'mymodule.state-one', [
'ui.state'
])
.config(function config($stateProvider) {
$stateProvider.state('state-one', {
url: '/state-one',
views: {
'main': {
controller: 'MyCtrl',
templateUrl: 'mytemplate.tpl.html'
}
}
});
})
.controller('MyCtrl',
function ($scope, $state) {
$scope.testVar = false;
$scope.myFunc = function () {
$scope.testVar = true;
$state.transitionTo('state-two');
};
}
);
describe('- mymodule.state-one', function () {
var MyCtrl, scope
beforeEach(module('mymodule.state-one'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
MyCtrl = $controller('MyCtrl', {
$scope: scope
});
}));
describe('- myFunc function', function () {
it('- should be a function', function () {
expect(typeof scope.myFunc).toBe('function');
});
it('- should test scope.testVar to true', function () {
scope.myFunc();
expect(scope.testVar).toBe(true);
expect(scope.testVar).not.toBe(false);
});
});
});
Disclaimer: I haven't done this myself, so I totally don't know if it will work and is what your are after.
From the top of my head, two solutions come to my mind.
1.) In your tests pre configure the $stateProvider to return a mocked state for the state-two That's also what the ui-router project itself does to test state transitions.
See: https://github.com/angular-ui/ui-router/blob/04d02d087b31091868c7fd64a33e3dfc1422d485/test/stateSpec.js#L29-L42
2.) catch and parse the exception and interpret it as fulfilled test if tries to get to state-two
The second approach seems very hackish, so I would vote for the first.
However, chances are that I totally got you wrong and should probably get some rest.
Solution code:
beforeEach(module(function ($stateProvider) {
$stateProvider.state('state-two', { url: '/' });
}));
I recently asked this question as a github issue and it was answered very helpfully.
https://github.com/angular-ui/ui-router/issues/537
You should do a $rootScope.$apply() and then be able to test. Note that by default if you use templateUrl you will get an "unexpected GET request" for the view, but you can resolve this by including your templates into your test.
'use strict';
describe('Controller: CourseCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
// load controller widgets/views/partials
var views = [
'views/course.html',
'views/main.html'
];
views.forEach(function(view) {
beforeEach(module(view));
});
var CourseCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
CourseCtrl = $controller('CourseCtrl', {
$scope: scope
});
}));
it('should should transition to main.course', inject(function ($state, $rootScope) {
$state.transitionTo('main.course');
$rootScope.$apply();
expect($state.current.name).toBe('main.course');
}));
});
Also if you want to expect on that the transition was made like so
expect(state.current.name).toEqual('state-two')
then you need to scope.$apply before the expect() for it to work

Resources