Preface : I'm quite new to Angular and very new to unit-testing. Be gentle.
I'm trying to run unit tests on a controller for a bootstrap modal window.
When initiating the modal I am passing through an object called thisVersion like so :
function showShareDialog(thisVersion){
var modalInstance = $modal.open({
templateUrl: 'app/shares/views/shares-dialogue.tpl.html',
controller: 'SharesCtrl',
resolve:{
thisVersion:function(){
return thisVersion;
}
}
});
modalInstance.result.then(function (validated) {
// .. more code
});
}
As soon as the Shares Controller is instantiated I am calling a method on thisVersion
thisVersion.getList('shares').then(function(result){
$scope.shares = result;
});
thisVersion is being passed in to the controller as a dependency, and that all works as expected.
The issue, however, is that I can't seem to inject it into my test suites. I keep getting this error
Error: [$injector:unpr] Unknown provider: thisVersionProvider <- thisVersion
This is (most of) my test suite :
var scope, controller, thisVersion;
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, _thisVersion_) {
scope = $rootScope.$new();
controller = $controller('SharesCtrl', {
$scope: scope
});
thisVersion = _thisVersion_;
}));
it('should get a list of all shares', function(){
expect(thisVersion.getList).not.toBe('undefined');
});
I know I am going to have to mock the call to the Api from thisVersion.getList() but for now I'm just concerned with getting the test suite to recognise thisVersion
You need to inject thisVersion into the $controller, seeing as how it's a resolved value in $modal.open.
var modalInstance = $modal.open({
templateUrl: 'app/shares/views/shares-dialogue.tpl.html',
controller: 'SharesCtrl',
resolve:{
// Resolved properties will be available for DI, into the controller.
thisVersion:function(){
return thisVersion;
}
}
});
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, _thisVersion_) {
scope = $rootScope.$new();
controller = $controller('SharesCtrl', {
$scope: scope,
thisVersion: _thisVersion_ /** inject the service _into_ the controller **/
});
}));
Related
First, I never seen Angular and Jasmine until several months ago. So I spend two or three months studying this in the practices of a company, and finally they've sent me to try test a controller/service in Visual Studio Code.
I have this variable in the controller:
vm.option = $state.param.option;
And in the spec.js I create a it with this:
it('"option" should be defined', function () {
expect(ctrl.option).toBeDefined();
});
Previously, I inject in beforeEach a $controller, $rootScope, _$log_, $injector and the service. I need something special for test this variable? I tried inject _$state_ but the message Expected undefined to be defined appears too.
I appreciate all help, and sorry for my bad english.
Edit:
The spec.js :
'use strict';
describe('app/specs/spec.js', function () {
var scope, $log, service, ctrl, state/*, testedStateExample*/;
beforeAll(function () {} );
beforeEach(angular.mock.module('App.moduleExample'));
beforeEach(function () {
module(function ($provide){
$provide.constnt('APP_CONFIG', {});
});
});
beforeEach(angular.mock.inject(function ($controller, $state, $rootScope, _$log_, _service_){
service = _service_;
scope = $rootScope.$new();
$log = _$log_;
$state = $state;
state = { params: { option: 'E' }}
ctrl = $controller('controllerExample', {
$scope: scope,
service: service,
$log: $log
});
//testedStateExample = new ctrl(state);
});
it('"option" should be defined', function () {
expect(state.params).toBeDefined();
});
});
There is typo in your controller: $state doesn't have param property but params.
Besides, you have to also define params.option on injected $state object in your tests because in injected $state it's rather not set so your controller can't read it from $state - but how to do this depends on your code details. state.params are set according to URL and route config but when you test standalone controller there is no URL nor route config and as a result $state.params is empty.
The best way to solve your problem is to mock injected $state:
Lets say that you have following controller:
function MyController($state) {
this.option = $state.params.option
...
}
In your test spec you can mock $state service and pass this mock to your controller as argument:
var mockState = {
params: {
option: 'TEST'
}
}
var testedControllerInstance = new MyController(mockState)
}
...
expect(testedControllerInstance.option).toBe('TEST');
Answer update according to updated question:
You've forgotten to inject state into your controller:
beforeEach(angular.mock.inject(function ($controller, $state, rootScope, _$log_, _service_){
service = _service_;
scope = $rootScope.$new();
$log = _$log_;
var state = { params: { option: 'E' }}
ctrl = $controller('controllerExample', {
$scope: scope,
service: service,
$log: $log,
$state: state
});
});
...
})
;
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.
In an effort to be more Angular 2.0 like I've decided to eliminate defining a controller in the traditional way:
e.g.
return {
restrict: 'E',
controller: 'MyCtrl',
controllerAs: 'vm',
bindToController: true,
scope: {},
templateUrl: 'path/to/tempate.html'
}
Here we can see that the controller name is passed as a string meaning that we have something like this defined somewhere:
app.controller('MyCtrl', function(...));
In a traditional test I'd just inject $controller service into the test to retrieve the MyCtrl controller. But I've decided to do this:
return {
restrict: 'E',
controller: MyCtrl,
controllerAs: 'vm',
bindToController: true,
scope: {},
templateUrl: 'path/to/tempate.html'
}
Here the only difference is that what is being passed to the controller declaration is a function called MyCtrl. This seems all well and good but do you go about retrieving this controller and testing it?
I tried doing this:
var $compile, $rootScope, $httpBackend, element, ctrl;
beforeEach(module('app'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_, _$httpBackend_) {
$compile = _$compile_;
$rootScope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
element = $compile('<directive></directive>')($rootScope);
ctrl = element[0].controller;
});
});
But in the above I get undefined coming back for the controller. Has anyone else made this move to be more like Angular 2.0? What differences have had to be made when it comes to testing?
Thanks
P.S The idea of the change in syntax is to make it easy to upgrade to Angular 2.0 when the time comes for us to do it.
EDIT
I've spent the last day or two trying to retrieve the controller and the only way that works seems to be just passing the controller in the traditional way. Here's a list of the ways I've tried (see the comments to determine what each outputs)
(function() {
'use strict';
/*
This part is used just for testing the html rendered
*/
describe('flRequestPasswordReset template: ', function() {
var $compile, $rootScope, $templateCache, template, element, ctrl;
var path = 'templates/auth/fl-request-password-reset/fl-request-password-reset.html';
beforeEach(module('app'));
beforeEach(module(path));
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$templateCache = _$templateCache_;
// Using nghtml2js to retrieve our templates
template = $templateCache.get(path);
element = angular.element(template);
$compile(element)($rootScope.$new());
$rootScope.$digest();
// $rootScope has a reference to vm which is what my
// controllerAs defines but only contains one of the variables
// This returns undefined
ctrl = element.controller('flRequestPasswordReset');
}));
it('should be defined', function() {
expect(element).toBeDefined();
});
});
describe('flRequestPasswordReset: template with controller: ', function() {
var $compile, $rootScope, $httpBackEnd, element, ctrl;
var path = '/templates/auth/fl-request-password-reset/fl-request-password-reset.html';
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_, _$httpBackend_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$httpBackEnd = _$httpBackend_;
$httpBackEnd.expectGET(path).respond({});
element = angular.element('<fl-request-password-reset></fl-request-password-reset>');
$compile(element)($rootScope.$new());
$rootScope.$digest();
// Uses alternate name for directive but still doesn't get the
// controller
ctrl = element.controller('fl-request-password-reset');
}));
it('should be defined', function() {
expect(element).toBeDefined();
});
});
/*
This part is used for testing the functionality of the controller by itself
*/
xdescribe('flRequestPasswordReset: just controller', function() {
var scope, ctrl;
beforeEach(module('app'));
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope;
// Instantiate the controller without the directive
ctrl = $controller('FlRequestPasswordResetController', {$scope: scope, $element: null});
}));
it('should be defined', function() {
expect(true).toBe(true);
});
/*
This works because I moved the controller back to the old way
*/
});
}());
According to my understanding of this question under pkozlowski.opensource answer you can use pass the controller as a function only if you don't encapsulate it inside the directive. This way it's exposed globally. This is a bad thing as it pollutes the global namespace however he seems to suggest wrapping it up when the application is built.
Is my understanding of this correct?
Angularjs docs give the usage of $controller service as:
$controller(constructor, locals);
Can anyone focus some light on these 2 points:
When to use $controller service. Please provide some use case.
Details about 'locals' parameter passed to it.
You can create common functions which are to be executed on $scope into one controller may be named 'CommonCtrl'.
angular.module('app',[]).controller('CommonCtrl', ['$scope', function($scope){
var self = this;
$scope.stuff1 = function(){
}
$scope.stuff2 = function(){
}
self.doCommonStuff = function(){
// common stuff here
$scope.stuff1();
$scope.stuff2();
};
return self;
}]);
And inject this controller in other controllers let say 'TestCtrl1' like
angular.module('app',[]).controller('TestCtrl1', ['$scope','$controller', function($scope, $controller){
var commonCtrl = $controller('CommonCtrl',{$scope: $scope}); // passing current scope to commmon controller
commonCtrl.doCommonStuff();
}]);
Here, the in second argument of $controller service, we are passing dependencies that are required by CommonCtrl. So the doCommonStuff method will use TestCtrl1 controller's scope.
To mention one, it is useful in creating the target controller during unit testing.
Lets say you have a controller with signature .controller('MainCtrl', function($scope, serviceA){..}).
In testing,
// ...
beforeEach(inject(function ($rootScope, $controller, serviceA) {
// assign injected values to test module variables
scope = $rootScope.$new();
service = serviceA
// create the controller, by passing test module variables values as dependencies
$controller('MainCtrl', {'$scope': scope, 'serviceA': service});
}));
it('test on controller', function() {
//...
});
For more info checkout: https://docs.angularjs.org/guide/unit-testing
You can also use this service to achieve controller inheritance.
angular.module('app',[]).controller('BaseCtrl', ['$scope', function($scope){
$scope.action1 = function(){
console.log('In BaseCtrl action1');
}
$scope.action2 = function(){
console.log('In BaseCtrl action2');
}
}]);
angular.module('app').controller('ChildCtrl', ['$scope', function($scope){
angular.extend(this, $controller('BaseCtrl', {
$scope: $scope
}));
$scope.action1 = function(){
console.log('Overridden in ChildCtrl action1');
}
$scope.action2 = function(){
console.log('Overridden in ChildCtrl action2');
}
}]);
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