AngularJS Karma test - Resolve object passed into controller is undefined when testing - javascript

We are using ui-router 0.2.10.
I am injecting a resolve object as a parameter into my controller, which is then setting a scope variable in the controller. It works perfectly on the app like so:
state provider
$stateProvider.state('myState', {
resolve:{
foo: function(){
return 'bar';
},
url: '/',
templateUrl: 'index.html',
controller: 'FooCtrl'
})
controller
app.Controllers.controller('FooCtrl', ['$scope', '$state', 'foo',
function ($scope, $state, $log, Zone, foo) {
$scope.testVar = foo
console.log($scope.testVar);
}])
'Bar' is then logged to the console as expected in Chrome.
But when running tests using Karma, the resolve object is now undefined, which fails the test. Here is the test code:
describe('controllers', function(){
var $rootScope,
$scope,
$state
beforeEach(module('app'))
beforeEach(inject(function($injector) {
$state = $injector.get('$state')
$rootScope = $injector.get('$rootScope')
$scope = $rootScope.$new()
$controller = $injector.get('$controller')
}))
it('FooCtrl should exist', inject( function() {
$state.go('myState')
$rootScope.$apply()
$controller = $controller('FooCtrl', {
'$scope': $scope
})
$rootScope.$apply()
assert.equal($scope.testVar, "bar", "these strings are equal")
}))
})
This error is presented (the resolve object in my case is called resolvedRouteModels):
[$injector:unpr] Unknown provider: fooProvider <- foo
http://errors.angularjs.org/1.3.0-build.2921+sha.02c0ed2/$injector/unpr?p0=fooProvider%20%3C-%20foo
Any help would be much appreciated, and please let me know if you have encountered this problem.

When you instantiate your controller, Angular usually can figure out how to satisfy the controller's dependencies. In this case, it doesn't know about UI-Router's "resolve" functionality.
One way to address this is to supply this dependency yourself in the test, the same way you are passing in the scope to the controller:
var foo = 'bar'; // whatever
$controller = $controller('FooCtrl', {$scope: $scope, foo: foo} );
Note, you could also create a mock $state object and pass that into the controller the same way, if you wanted to incorporate that into your tests.

my assumption is your Angular set up is perfect, if that's the case, you might want to test your code this way. I've used Jasmine 2 syntax.
describe('Foo Controller', function() {
var $scope, $state, controller, Zone, foo, $log;
beforeEach(module('app'));
beforeEach(inject(function($controller) {
$scope = {};
$state = {};
$log = {};
Zone = {};
foo = {};
controller = $controller;
}));
it('should log the value foo', function() {
spyOn(console, 'log');
controller('FooCtrl', { $scope, $state, $log, Zone, foo });
expect($scope.testVar).toEqual({});
expect(console.log).toHaveBeenCalledWith({});
});
it('should log the value foo', function() {
spyOn(console, 'log');
// You could change the value of foo i.e.
foo = 'create more spies than fbi';
controller('FooCtrl', { $scope, $state, $log, Zone, foo });
expect($scope.testVar).toEqual('create more spies than fbi');
expect(console.log).toHaveBeenCalledWith('create more spies than fbi');
});
});
Once again I hope this helps. Peace.

Related

Jasmine spyOn not working properly on AngularJS directive

I'm working on an AngularJS app and I'm facing some problems with Jasmine's SpyOn in a concrete directive.
The directive is quite simple, just call a service's method and when it resolves/rejects the promise acts in consequence, setting some values or another ones.
The problem: When I try to mock SignatureService.getSignatureData SpyOn does not work as I expect, and acts as if I was invoking jasmine's callThrough method over getSignatureData.
I've been using spyOn and mocks in other directives and services, and there was no problem with those.
I've been trying to solve this issue the last two days, comparing with other solutions and user's answers, but I can not find a valid solution.
Here's my code:
AngularJS directive code:
angular
.module('module_name')
.directive('signatureDirective', signatureDirective);
angular
.module('GenomcareApp_signature')
.controller('signatureDController', signatureDController);
function signatureDirective() {
return {
restrict: 'E',
templateUrl: 'components/signature/signature.directive.html',
controller: signatureDController,
controllerAs: 'ctrl',
bindToController: true
};
}
signatureDController.$inject = [
'$scope',
'$rootScope',
'$location',
'SignatureService'
];
function signatureDController($scope, $rootScope, $location, SignatureService) {
var controller = this;
$scope.$on('pdfFileLoadSuccessfully', function (data) {
console.log(data);
controller.loadPdfSucceed = true;
});
$scope.$on('pdfFileLoadFails', function (data) {
console.error(data);
controller.loadPdfError = true;
});
function loadDirectiveInitData() {
var queryParameters = atob($location.search().data);
controller.email = queryParameters.split(';')[0];
controller.phone = queryParameters.split(';')[1];
controller.docid = queryParameters.split(';')[2];
SignatureService.getSignatureData(controller.email, controller.phone, controller.docid)
.then(
function (data) {
console.log(data);
controller.stampTime = data.stamp_time;
controller.fileUrl = data.original_file.url;
},
function (error) {
console.error(error);
controller.error = true
})
.finally(
function () {
controller.endLoad = true;
})
}
loadDirectiveInitData();
}
Jasmine test code:
'use strict';
/* global loadJSONFixtures */
describe('Test :: Signature directive', function () {
beforeEach(angular.mock.module('app'));
beforeEach(module('translateNoop'));
var $q, $compile, $rootScope, controller, $scope, $httpBackend, $location, SignatureService;
beforeEach(angular.mock.inject(function (_$controller_, _$q_, _$rootScope_, _$location_, _$compile_, _$httpBackend_, _SignatureService_) {
$q = _$q_;
$compile = _$compile_;
$location = _$location_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
SignatureService = _SignatureService_;
spyOn($location, 'search').and.returnValue({data: 'dGVzdEB0ZXN0LmNvbTsrMzQ2NjY2NjY2NjY7WG9TUFFnSkltTWF2'});
$httpBackend.whenGET('components/signature/signature.directive.html').respond(200, '');
controller = _$controller_('signatureDController', {$scope: $scope});
}));
describe('Testing directive', function () {
it('Init data should be set when promise resolves/rejects', function (done) {
// SpyOn DOES NOT MOCK THE SERVICE METHOD
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
return $q.resolve({...})
});
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
done();
// ... some expect stuff
});
});
});
If any one can give me some advice or solution, I would be very thankful.
Thank you very much.
UPDATE1: I don't know why, but if I do not declare the controller variable in the global beforeEach, Jasmine's spyOn mocks the method as I expect.
Now the issue is how to get the controller to test that the controller values are set as expected.
Well... I realized that the problem was that the controller was being created before all, and somehow when the service was mocked the controller ignores it.
This idea came by accident, when I paste the service's spyOn in the global beforeEach.
So I decide to create a new instance of the controller and the corresponding spyOn with the desired result inside the beforeEach of each describe.
It works. Maybe it's not the best aproach, and I encourage to anyone who have the answer to post it. I'm going to be eternally greatful.
Here's my final test code:
describe('Test :: Signature directive', function () {
beforeEach(angular.mock.module('app'));
beforeEach(module('translateNoop'));
var $q, $compile, $rootScope, $scope, $httpBackend, $location, SignatureService, test_fixture;
beforeEach(angular.mock.inject(function (_$q_, _$rootScope_, _$location_, _$compile_, _$httpBackend_, _SignatureService_) {
$q = _$q_;
$compile = _$compile_;
$location = _$location_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
SignatureService = _SignatureService_;
// controller = _$controller_;
spyOn($location, 'search').and.returnValue({data: 'dGVzdEB0ZXN0LmNvbTsrMzQ2NjY2NjY2NjY7WG9TUFFnSkltTWF2'});
$httpBackend.whenGET('components/signature/signature.directive.html').respond(200, '');
}));
describe('Testing directive when service resolve promise', function () {
var controller;
beforeEach(inject(function(_$controller_) {
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
return $q.resolve({...})
});
controller = _$controller_('signatureDController', {$scope: $scope})
}));
it('Init data should be set', function () {
// spyOn($location, 'search').and.callThrough();
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
// ... some expect(...).toEqual(...) stuff and more
});
});
});
Thank you for your time.
Try to use $q.defer(), here's an example:
it('Init data should be set when promise resolves/rejects', function (done) {
// SpyOn DOES NOT MOCK THE SERVICE METHOD
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
let deferred = $q.defer();
deferred.resolve({...});
return deferred.promise;
});
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
done();
// ... some expect stuff
});

Unknown Provider: $scopeProvider When Testing Angular Controller With Jasmine

I am trying to implement unit testing for a particular controller of a web app using Jasmine and Karma. At the moment it is giving the following error:
Chrome 53.0.2785 (Mac OS X 10.10.5) HomeCtrl should be defined FAILED
Error: [$injector:unpr] Unknown provider: $scopeProvider <- $scope
http://errors.angularjs.org/1.5.5/$injector/unpr?p0=%24scopeProvider%20%3C-%20%24scope
Here is the code of the testing file:
describe('HomeCtrl', function(){
var $controller, HomeCtrl;
beforeEach(angular.mock.module('TestModule'));
beforeEach(inject(function(_$controller_, _$rootScope_, _$scope_) {
$controller = _$controller_;
rootScope = $rootScope.new();
scope = $scope.new();
HomeController = $controller('HomeCtrl', {
$scope: scope
});
}));
// Verify our controller exists
it('should be defined', function() {
expect(HomeController).toBeDefined();
});
});
Could someone tell me what I am doing wrong?
In AngularJS, all scopes are children of $rootScope.
In unit tests, you cannot inject $scope since, there is no service that exists. But there is an $rootScope provider which contains API like $new to create a new child scope.
$rootScope.$new(), create a new child scope.
Since, you cannot inject $scope you are getting (no such provider exists to provide you $scope)
Error: [$injector:unpr] Unknown provider: $scopeProvider <- $scope
In the answer, you have provided,
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
HomeCtrl = $controller('HomeCtrl', {
$scope: $rootScope,
});
}));
You are injecting $rootScope and directly passing the $rootScope to HomeCtrl. It works, but while executing tests it adds all variables and function in controller code to $rootScope.
But in real scenario, your HomeCtrl is expecting a $scope(child scope). So, to replicate the actual scenario, it would be better if you pass a child scope.
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$scope = _$rootScope_.$new();
HomeCtrl = $controller('HomeCtrl', {
$scope: $scope,
});
}));
The following code succeeds:
describe('HomeCtrl', function(){
var $controller, HomeCtrl;
var $rootScope, $scope;
beforeEach(angular.mock.module('TestModule'));
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
HomeCtrl = $controller('HomeCtrl', {
$scope: $rootScope,
});
}));
// Verify our controller exists
it('should be defined', inject(function($controller) {
expect(HomeCtrl).toBeDefined();
}));
});

How to test my http request in my app

I am trying to write units test for my app and I have the following issue
In my controller, I have something like
$scope.test1 = function() {
productFactory.getName()
.then(function(products){
$scope.result = products;
})
}
productFactory
angular.module('myApp').factory('productFactory', function($http) {
var factoryObj = {};
factoryObj.getName = function() {
return http.get(url)
}
return factoryObj
})
In my unit test file
describe('test here', function () {
var testCtrl, scope, httpBackend, mockFactory;
beforeEach(module('myApp', function($provide){
$provide.value('productFactory', mockFactory);
}));
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _productFactory_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
mockFactory = _productFactory_;
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
it('should get product name', function() {
scope.test1();
//I am not sure how to test the results
});
}));
When I run karma test, it gives me
TypeError: 'undefined' is not an object (evaluating 'productFactory.getName()')
I am not sure how to test the http result and fix the error. Can anyone help me about it? Thanks a lot!
First of all, you don't need to worry about using $provide:
beforeEach(module('myApp'));
1. Without $httpBackend (mock out the service completely)
Then, productFactory will be passed into your controller, but you want to spyOn the getName():
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _productFactory_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
mockFactory = _productFactory_;
// add spy for the method, wrap with $q.when so it returns a promise
spyOn(mockFactory, 'getName').and.returnValue($q.when('Pizza!'));
testCtrl = _$controller_('testCtrl', {
$scope: scope,
productFactory: mockFactory // pass in here
});
Then, you've got to cause a $digest cycle, so that the promise will call through:
it('should get product name', function() {
scope.test1();
// hit the $digest
scope.$apply();
// expectation
expect(scope.result).toBe('Pizza!')
});
2. With $httpBackend
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
// set up httpBackent
httpBackend.when('GET', '/products')
.respond([{ name: 'Pizza!'}, {name: 'Sandwich'}]);
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
We don't need to mock the factory in this case at all. Then, we just need to flush $httpBackend when we want the http call to return:
it('should get product name', function() {
scope.test1();
// hit the $digest with flush
httpBackend.flush();
// expectation
expect(scope.result.length).toBe(2)
});

Jasmine unit test $scope access

Why am I unable to access my function in my Controller? The code functions like I would expect it too, however, it doesn't seem to want to allow me access to my function that I'm trying to unit test. It should just return a simple bool, but it's getting killed somewhere.
Here's some code:
RTHelper.js
describe('Unit: LocationController', function () {
var $scope, $httpBackend, $location, injector, ctrl, $controller;
//beforeEach(function () {
// angular.module('TDE').controller('LocationController'); //
// inject(function ($injector) {
// $rootScope = $injector.get('$rootScope');
// $scope = $rootScope.$new();
// //ctrl = $injector.get('$controller')("LocationController", { $scope: $scope });
// injector = $injector;
// ctrl = $injector.get('$controller');
// //scope = $injector.get('$rootScope').$new();
// $httpBackend = $injector.get('$httpBackend');
// $location = $injector.get('$location');
// });
//});
//both beforeEach methods work(which one is better? I don't know), so things are getting loaded
beforeEach(function () {
angular.module('TDE');
inject(function ($injector) {
$location = $injector.get('$location');
$rootscope = $injector.get('$rootScope');
$scope = $rootscope.$new();
$controller = $injector.get('$controller');
ctrl = function () {
return $controller('LocationController', {
'$scope': $scope
})
};
})
});
it("should just be a holder for something for later", function () {
expect($scope.BoolCondition()).toBeDefined(); //I don't care what it returns as long as it's accessed honestly
});
})
LocationController.js
angular
.module('TDE')
.controller('LocationController', ['$rootScope', '$scope', '$location', '$window', '$document', 'LocationService', 'HeaderFooterService', 'SearchService', 'TranslationService', 'MTDE_CONFIG', 'LocationPartnerAssignmentService', 'ExperimentService', function ($rootScope, $scope, $location, $window, $document, $LocationService, $HeaderFooterService, $SearchService, $TranslationService, $MTDE_CONFIG, $LocationPartnerAssignmentService, $ExperimentService) {
$scope.BoolCondition = function(myCondition){
if(//blah blah condition test on myCondition)
{
return true
}
else
{
return false;
}
}
How would I go about getting to that BoolCondition? I'm new to this so you can imagine the struggle of writing unit tests after never having done unit testing. I've also gone through countless examples and I've done some generic tests, so I'm not totally un-versed.
You're not bootstrapping the module under test correctly. You should use angular.mock.module inside the test
You're not instantiating the controller (where's the call to ctrl()?)
Here's the complete working example and the fiddle that runs it:
describe('Unit: LocationController', function () {
var $scope, $location, ctrl;
beforeEach(function () {
angular.mock.module('TDE');
inject(function (_$location_, _$rootScope_, _$controller_) {
$location = _$location_;
$scope = _$rootScope_.$new();
ctrl = _$controller_('LocationController', {
'$scope': $scope
})
});
});
it("should just be a holder for something for later", function () {
expect($scope.BoolCondition()).toBeDefined();
expect($scope.BoolCondition()).toBeTruthy();
});
})

Howto inject AngularJS controller with lots of dependencies

I am trying to test one of my controllers. My Problem is that the created $LoginController object is always an empty object and instead my methods are all on the scope object. Then when I want to test the
hasError function, I get a message:
TypeError: 'undefined' is not an object (evaluating '$route.current.params')
Here is my test:
describe('login', function () {
var $LoginController = null, $scope, $route, $location, $httpBackend;
beforeEach(module('common.base64'));
beforeEach(module('myapp.user'));
beforeEach(inject(function ($injector, $controller, $rootScope,
_$location_, _$timeout_, _$route_, _Base64_)
{
$httpBackend = $injector.get('$httpBackend');
$scope = $rootScope.$new();
$route = _$route_;
$location = _$location_;
$LoginController = $controller('LoginController', {
$scope : $scope, $http: $httpBackend, $location: _$location_,
$timeout: _$timeout_, $route: _$route_, Base64: _Base64_}
);
console.log($LoginController); // <-- Log: {}
console.log($scope); // <-- Log: { <lots of stuff> }
}));
describe('LoginController', function() {
it('should have been injected', inject(function () {
expect($LoginController).toBeTruthy();
}
));
});
describe('hasError', function () {
it('should return error', inject(function () {
$location.url('/login?loginError=true');
expect($scope.hasError).toBeTruthy();
// ^-- hasError is defined!
expect($LoginController.hasError).toBeTruthy();
// ^-- hasError is undefined!
}
));
}
);
}
);
In my LoginController the hasError function is defined like this:
$scope.hasError = function () {
if($route.current.params) {
if ($route.current.params['loginError']) {
return "error";
} else {
return "";
}
}
return "";
};
Which I then inject into an elements class attribute. So which piece of my code is wrong, the controller, or the test?
Your test is.
Since you are not using any routing in your test here, there will not be a current route to pull from.
You can explicitly set this up if you need to
_$route_.current = {
params: {
loginError: 12345
}
};
$location and several other things all get swapped out with mocks if you are using the ng-mock module... which it appears you are.
So changing the $location isn't going to trigger a route change.
A better solution would be to change your controller to use the $routeParams dependency instead of accessing the current route itself. This way you can just pass that in during your test creation.
if($routeParams && $routeParams.loginError) {
return "error";
}
return "";
And then in your tests, you can simply pass in the $routeParams
$LoginController = $controller('LoginController', {
$scope : $scope, $http: $httpBackend, $location: _$location_,
$timeout: _$timeout_,
// Mock out $routeParams here:
$routeParams: {loginError: 'blah' },
Base64: _Base64_}
);

Categories