How to mock $compile function when testing a factory with Jasmine - javascript

My problem is very similar to this post but just different enough that the accepted answer doesn't work for me. Essentially, I have an Angular factory which makes use of $compile, which returns a promise, and I want to mock it in Jasmine. Here is a simplified version of the factory:
angular.module('app.common')
.factory('myFactory', myFactory);
myFactory.$inject = ['$compile', '$rootScope'];
function myFactory($compile, $rootScope) {
var factory = {
testFunc: testFunc
}
return factory;
function testFunc(stuff) {
angular.element(document.body).append($compile(stuff)($rootScope));
}
}
And here is my test:
describe("Common", function() {
// I have many common services
beforeEach(function() {
angular.mock.module('app.common');
});
describe("Factory Example: ", function() {
var mockCompile, mockRoot, aFactory;
beforeEach(function() {
module(function($provide) {
$provide.value('$compile', jasmine.createSpy('$compile'));
$provide.value('$rootScope', jasmine.createSpy('$rootScope'));
});
});
beforeEach(inject(function($compile, $rootScope, myFactory, $q) {
mockCompile = $compile;
mockCompile = function() {
var deferred = $q.defer();
deferred.resolve('remote call result');
return deferred.promise;
};
mockRoot = $rootScope;
aFactory = myFactory;
}));
it('should work', function() {
aFactory.testFunc('stuff');
expect(true).toBe(true);
});
});
});
This code is complaining at me that $compile does not return a promise. It appears that $provide doesn't know about the new function assignment from inject. Ive been hacking around at it but Im new to Jasmine so I dont really know what Im doing. Any help would be appreciated - and if there's an easier way to do what Im trying to do please let me know!

Okay, hooray I solved it!
For those who may be interested in the future:
The first problem was I was incorrectly assuming that $compile returns a promise, which it does not. It returns a function which takes the scope as an argument. If you do need to mock a function that returns a promise, check out this post which helped me figure out how.
Of course, that didn't work for me because all I needed to do was have my mocked $compile return a function. The following code worked for me:
describe("Common", function() {
// I have many common services
beforeEach(function() {
angular.mock.module('app.common');
});
describe("Factory Example: ", function() {
var mockCompile, mockRoot, aFactory;
beforeEach(function() {
module(function($provide) {
// Just use callFake to have $compile return a function
$provide.value('$compile', jasmine.createSpy().and.callFake(function() {
return function(scope) { scope(); };
});
$provide.value('$rootScope', jasmine.createSpy());
});
});
beforeEach(inject(function($compile, $rootScope, myFactory, $q) {
mockCompile = $compile;
mockRoot = $rootScope;
aFactory = myFactory;
}));
it('should work', function() {
aFactory.testFunc('stuff');
expect(mockCompile).toHaveBeenCalled();
expect(mockRoot).toHaveBeenCalled();
});
});

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
});

Issue with jasmine spies call through

I am having trouble calling through to the actual implementation and I am getting this error:
TypeError: undefined is not an object (evaluating 'GitUser.GetGitUser('test').then') ...
Here are my codes:
app.controller('HomeController', ['$scope', 'GitUser', function ($scope, GitUser) {
$scope.name = "user";
GitUser.GetGitUser('test').then(function (data) {
console.log(data);
if (data) {
$scope.name = data;
}
});
}]);
app.factory('GitUser', function ($http) {
return {
GetGitUser: function (username) {
return $http.get('https://api.github.com/users/' + username)
.then(function success(response) {
return response.data.login;
});
}
};
});
Here is my unit test:
describe('HomeController Unit Test', function () {
var $controllerConstructor, scope;
beforeEach(module("AngularApp"));
beforeEach(inject(function ($controller, $rootScope) {
$controllerConstructor = $controller;
scope = $rootScope.$new();
}));
it('should test if scope.name is test', function () {
// Act
GitUser = {
GetGitUser: function () { }
};
spyOn(GitUser, "GetGitUser").and.callThrough();
GitUser.GetGitUser();
$controllerConstructor('HomeController', {
'$scope': scope,
'GitUser': GitUser
})
// Assert
expect(GitUser.GetGitUser).toHaveBeenCalled();
expect(scope.name).toBe('test');
});
});
The problem is a bit more complex than just a missing inject ...
Here's an adjusted test:
https://plnkr.co/edit/ZMr0J4jmLPtDXKpRvGBm?p=preview
There are a few problems:
1) you are testing a function that returns a promise - so you need to also mock it that way (by using return $q.when(..) for example).
2) you are trying to test code that happens when your controller is created - the
GitUser.GetGitUser('test').then(function (data) {
console.log(data);
if (data) {
$scope.name = data;
}
});
should be wrapped in a function instead:
function init() {
GitUser.GetGitUser('test').then(function (data) {
console.log(data);
if (data) {
$scope.name = data;
}
});
}
and then make that available on your scope:
scope.init= init;
Then in your test call the function and verify your assertions. If you don't wrap it in a function it won't be testable.
Also - the mocking and the callThrough thing ... as you are testing the controller (and not the service) you can use callFake instead - the callFake function can return a Promise with a value (the one that you want to verify later) - then you can ensure that the controller part of the puzzle works.
var name = 'test';
// instead of trying to mock GitUser you can just callFake and be sure to return a promise
spyOn(GitUser, "GetGitUser").and.callFake(function() {
return $q.when(name);
});
I hope this all makes sense - the plunker should make things clear - I will add some more comments there.
I think you just miss something here
beforeEach(inject(function ($controller, $rootScope, _GitUser) {
$controllerConstructor = $controller;
scope = $rootScope.$new();
GitUser = _GitUser;
}));

How to spy on anonymous function using Jasmine

I'm using Jasmine to test my angular application and want to spy on an anonymous function.
Using angular-notify service https://github.com/cgross/angular-notify, I want to know whether notify function have been called or not.
Here is my controller:
angular.module('module').controller('MyCtrl', function($scope, MyService, notify) {
$scope.isValid = function(obj) {
if (!MyService.isNameValid(obj.name)) {
notify({ message:'Name not valid', classes: ['alert'] });
return false;
}
}
});
And here is my test:
'use strict';
describe('Test MyCtrl', function () {
var scope, $location, createController, controller, notify;
beforeEach(module('module'));
beforeEach(inject(function ($rootScope, $controller, _$location_, _notify_) {
$location = _$location_;
scope = $rootScope.$new();
notify = _notify_;
notify = jasmine.createSpy('spy').andReturn('test');
createController = function() {
return $controller('MyCtrl', {
'$scope': scope
});
};
}));
it('should call notify', function() {
spyOn(notify);
controller = createController();
scope.isValid('name');
expect(notify).toHaveBeenCalled();
});
});
An obviously return :
Error: No method name supplied on 'spyOn(notify)'
Because it should be something like spyOn(notify, 'method'), but as it's an anonymous function, it doesn't have any method.
Thanks for your help.
Daniel Smink's answer is correct, but note that the syntax has changed for Jasmine 2.0.
notify = jasmine.createSpy().and.callFake(function() {
return false;
});
I also found it useful to just directly return a response if you only need a simple implementation
notify = jasmine.createSpy().and.returnValue(false);
You could chain your spy with andCallFake see:
http://jasmine.github.io/1.3/introduction.html#section-Spies:_andCallFake
//create a spy and define it to change notify
notify = jasmine.createSpy().andCallFake(function() {
return false;
});
it('should be a function', function() {
expect(typeof notify).toBe('function');
});
controller = createController();
scope.isValid('name');
expect(notify).toHaveBeenCalled();

How to use $httpBackend.passThrough() with Karma unit tests

I've looked up and down and tried all kinds of things to make the E2E passThrough() actually work. Only examples are for templates. I don't think it actually can work but if someone has ever seen a working example I would love to see it. Basically I want real XHR calls in my Karma tests instead of mocks.
This is where I am at - i've tried a bunch of variations - right now I get an Unknown provider: cartServiceProvider error - sometimes I get that injected but then it's something else.
myAppDev = angular.module('myApp', ['ngMockE2E']);
myAppDev.run(function($httpBackend) {
$httpBackend.whenPOST(/^api\/cart\/save/).passThrough();
});
beforeEach(angular.module('myApp'));
beforeEach(inject(function(_$httpBackend_, _$rootScope_, _$http_e) {
$scope = _$rootScope_;
$http = _$http_;
$httpBackend = _$httpBackend_;
}));
describe('#createOrder()', function() {
it('should return an order ID', function(done) {
inject(function(cartService) {
$scope.$apply(function() {
cartService.createOrder(function(res) {
done();
});
});
})
});
});
Sample service:
angular.module('myApp').factory('cartService', function($http) {
Cart.prototype.createOrder = function(callback) {
var self = this;
var data = {}
$http.post('/api/cart/save', data ).success(function(res) {
self.order = res.id;
callback(res);
});
};

Testing an Angular Promise with Jasmine

The following test keeps failing and I can't figure out why? I am trying to figure out how to test defereds/promises with Jasmine.
Error
Expected undefined to be 'Resolved Data'.
Test
describe('Queued Repository', function () {
var ctrl,
rootScope,
scope,
service;
beforeEach(function () {
module('testApp');
inject(function ($rootScope, $controller, TestSrvc) {
rootScope = $rootScope;
scope = $rootScope.$new();
service = TestSrvc;
});
});
afterEach(inject(function ($rootScope) {
$rootScope.$apply();
}));
it('test something', function () {
expect(service.calculate(1, 5)).toBe(6);
});
it('resolves promises', function () {
var result;
service.getPromise().then(function (data) {
result = data;
});
rootScope.$apply();
expect(result).toBe('Resolved Data');
});
});
Service
var app = angular.module('testApp', []);
app.service('TestSrvc', ['$q', '$timeout', '$http', function ($q, $timeout, $http) {
return {
getPromise: function () {
var d = $q.defer();
$timeout(function () {
d.resolve('Defered Result');
}, 5000);
return d.promise;
},
getSomething: function () {
return "Test";
},
calculate: function (x, y) {
return x + y;
}
}
}]);
Try calling $timeout.flush() before expect(result).toBe('Resolved Data');.
In your example, you will need to call both $timeout.flush() AND $rootScope.$apply().
Explanation: $timeout.flush() will force your $timeout in the service to run immediately. Your service will then call 'resolve' - but the promise.then() will not be called until the subsequent digest cycle; therefore you will need to call $rootScope.$apply() to propagate any 'resolves' and 'watches' - which will occur synchronously.
NOTE: In Jasmine, ensure that your promise.then() function appears BEFORE your call to $rootScope.$apply otherwise it will not fire the promise.then() function. (I haven't figured out why this is the case in Jasmine.)

Categories