Mocking ngResource in Angular unit tests - javascript

I have an ngResourceMockFactory which looks like this:
(function() {
'use strict';
angular.module('app')
.factory('NgResourceMock', ngResourceMockFactory)
;
ngResourceMockFactory.$inject = [];
function ngResourceMockFactory() {
function NgResourceMock() {
var context = this;
context.$promise.then = function() {
context.prototype.$promise.then.apply(context, arguments);
};
context.$promise.finally = function() {
context.prototype.$promise.finally.apply(context, arguments);
};
}
NgResourceMock.prototype.$promise = {
then: function(onSuccess, onError) {
this.$promise.onSuccess = onSuccess;
this.$promise.onError = onError;
},
finally: function(onComplete) {
this.$promise.onComplete = onComplete;
}
};
return NgResourceMock;
}
})();
I inject this into my tests in a beforeEach like so:
beforeEach(inject(function(NgResourceMock) {
ngResourceMock = new NgResourceMock();
}));
then I use it like this:
describe('initiateWorkflow function', function() {
beforeEach(function() {
vm.player = {id: 123};
spyOn(dataService, 'initiateWorkflow').and.returnValue(ngResourceMock);
vm.initiateWorkflow();
});
it('should call dataService.initiateWorkflow', function() {
expect(dataService.initiateWorkflow).toHaveBeenCalledWith({playerId: vm.player.id}, {});
});
});
but I keep seeing the following error:
TypeError: 'undefined' is not an object (evaluating 'context.prototype.$promise')
This leads me to believe that something is wrong with my ngResourceMockFactory, but I'm not sure what it is.

Don't know if this can be of any help, but if you are trying to evaluate asynchronous operations in your tests, you may want to use the done() method in Jasmine.
As per their documentation:
beforeEach(function(done) {
setTimeout(function() {
value = 0;
done();
}, 1);
});
by passing done as a parameter of the beforeEach callback, any test run after the before each will wait until the done() function has been called.
Source: Jasmine (Asynchronous Support section).
Hope this helps.

Here is the solution to your problem.
The error TypeError: 'undefined' is not an object (evaluating 'context.prototype.$promise') is caused when you try to invoke the promise object before invoking the function into which it is defined or into which your parent function is defined.
Here the returnValue(ngResourceMock) is directly calling into the function without the context and parameters need to be defined.
Therefore you can try to add another beforeEach statement like
beforeEach(angular.mock.module(app));
to load your app module
Here may be the same concept related to your problem another link here.
Hope it may help you a bit.

Related

Angular factory not being instantiated

I have angular 1.4 app. Im using require.js. An included script have this:
define(['firstFactory', 'secondFactory'],
function(firstFactory, secondFactory){
var app = angular.module('services', []);
app.factory('firstFactory', firstFactory);
app.factory('secondFactory', secondFactory);
});
Inside require.config (in main.js) I have path to both the factories:
"firstFactory" : "factory/firstFactory",
"secondFactory" : "factory/secondFactory"
Finally in my main controller I have this:
function myCtrl(firstFactory, secondFactory) {
Thing is that in firstFactory.js I have this code:
define([], function () {
console.log('inside define firstFactory');
var firstFactory = function () {
console.log('inside function firstFactory');
};
return firstFactory;
});
While in the secondFactory.js I have this:
define([], function () {
console.log('inside define secondFactory');
var secondFactory = function () {
console.log('inside function secondFactory');
};
return firstFactory;
});
In the console I see:
inside define firstFactory
inside function firstFactory
inside define secondFactory
So I dont see
inside function secondFactory
Why? To me the symptom is that angular is not instantiating the second factory. I cannot understand that for the first one it is being instantiated but not for the second, as Im doing the same thing with both. Any clue?
In the secondFactory.js you are returning firstFactory, may be this is the reason.

How to sinon stub a call to external module in a tested function

I am testing a function. That function has a call to an external module which makes my test fail:
element.funcCall();
Where the function looks like this:
function funcCall() {
external.a.b.c.doSomething();
}
It fails on: undefined is not an object (evaluating 'external.a.b.c.doSomething')
How can I stub or fake this so the call is not really make. I do not need it's functionality for this test.
Here's how I do this kind of thing:
var element = require('element'),
external = require('external'),
sinon = require('sinon'),
expect = require('chai').expect;
describe('element.funcCall()', function() {
before(function() {
this.doSomethingStub = sinon.stub(external.a.b.c, 'doSomething');
});
it('should do something external', function() {
element.funcCall();
expect(this.doSomethingStub.called).to.be.true;
});
after(function() {
this.doSomethingStub.restore();
});
});
Here is how I ended up solving this. Since I did not care about that external function being executed, I faked it like so:
var func = function() {};
external =
{ a:
{ b:
{ c:
{doSomething: func}
}
}
}
...
element.funcCall();
expect.....
This was the internal function call was redirected to en empty function and I could test the rest of the method

Reactive variable is undefined when it is defined

I would like to get some help debugging a situation where a Reactive Variable is undefined, when it has been defined already.
This code is attaching a Reactive Variable to the template instance, and using the variable in template.autorun().
Template.home.onCreated(function () {
this.limit = new ReactiveVar(15);
this.autorun(function () {
this.subscribe('recent-topics', this.limit.get());
});
});
When I load the template for the first time, I expect the template to subscribe to recent-topics with an argument 15. However, the code throws an error:
Uncaught TypeError: Cannot read property 'get' of undefined
Any ideas why?
Just an answer for the sake of spreading the joys of ES6:
Template.home.onCreated(function () {
this.limit = new ReactiveVar(15);
this.autorun(() => {
this.subscribe('recent-topics', this.limit.get());
});
});
Make sure you add the grigio:babel package, and your Javascript file ends in .es6.js, .es6, or .jsx.
Explanation
In ES6 (aka ECMAScript 6), there's a new "fat arrow" syntax which is very similar to CoffeeScript's implementation. In ES6, when you do something like this:
someFunc = function () {
anotherThing((var1, var2) => {
this.thing = true;
});
};
It's the same as doing this:
someFunc = function () {
var self = this;
anotherThing(function (var1, var2) {
self.thing = true;
});
};
This is a scoping issue.
Inside of your Tracker.autorun, this no longer refers to the template, but the autorun's callback function. Inside of the autorun, try calling Template.instance().limit.get().
Better than using Template.instance().limit.get() (ryan's answer)
You should do something like this:
Template.home.onCreated(function () {
var self = this;
self.limit = new ReactiveVar(15);
self.autorun(function () {
self.subscribe('recent-topics', self.limit.get());
});
});

How do you mock an angularjs $resource factory

I have a resource factory
angular.module('mean.clusters').factory('Clusters', ['$resource',
function($resource) {
return $resource('clusters/:clusterId/:action', {
clusterId: '#_id'
}, {
update: {method: 'PUT'},
status: {method: 'GET', params: {action:'status'}}
});
}]);
and a controller
angular.module('mean.clusters').controller('ClustersController', ['$scope',
'$location', 'Clusters',
function ($scope, $location, Clusters) {
$scope.create = function () {
var cluster = new Clusters();
cluster.$save(function (response) {
$location.path('clusters/' + response._id);
});
};
$scope.update = function () {
var cluster = $scope.cluster;
cluster.$update(function () {
$location.path('clusters/' + cluster._id);
});
};
$scope.find = function () {
Clusters.query(function (clusters) {
$scope.clusters = clusters;
});
};
}]);
I am writing my unit tests and every example I find is using some form of $httpBackend.expect to mock the response from the server, and I can do that just fine.
My problems is, when unit testing my controller functions I would like to mock the Clusters object. If I'm using $httpBackend.expect, and I introduce a bug in my factory every unit test in my controller will fail.
I would like to have my test of $scope.create test only $scope.create and not also my factory code.
I've tried adding a provider in the beforeEach(module('mean', function ($provide) { part of my tests but I cant seem to get it right.
I also tried
clusterSpy = function (properties){
for(var k in properties)
this[k]=properties[k];
};
clusterSpy.$save = jasmine.createSpy().and.callFake(function (cb) {
cb({_id: '1'});
});
and setting Clusters = clusterSpy; in the before(inject but in the create function, the spy gets lost with
Error: Expected a spy, but got Function.
I have been able to get a spy object to work for the cluster.$update type calls but then it fails at var cluster = new Clusters(); with a 'not a function' error.
I can create a function that works for var cluster = new Clusters(); but then fails for the cluster.$update type calls.
I'm probably mixing terms here but, is there a proper way to mock Clusters with spies on the functions or is there a good reason to just go with $httpBackend.expect?
Looks like I was close a few times but I think I have it figured out now.
The solution was the 'I also tried' part above but I was not returning the spy object from the function.
This works, it can be placed in either the beforeEach(module( or beforeEach(inject sections
Step 1: create the spy object with any functions you want to test and assign it to a variable that's accessible to your tests.
Step 2: make a function that returns the spy object.
Step 3: copy the properties of the spy object to the new function.
clusterSpy = jasmine.createSpyObj('Clusters', ['$save', 'update', 'status']);
clusterSpyFunc = function () {
return clusterSpy
};
for(var k in clusterSpy){
clusterSpyFunc[k]=clusterSpy[k];
}
Step 4: add it to the $controller in the beforeEach(inject section.
ClustersController = $controller('ClustersController', {
$scope: scope,
Clusters: clusterSpyFunc
});
inside your tests you can still add functionality to the methods using
clusterSpy.$save.and.callFake(function (cb) {
cb({_id: '1'});
});
then to check the spy values
expect(clusterSpy.$save).toHaveBeenCalled();
This solves both problems of new Clusters() and Clusters.query not being a function. And now I can unit test my controller with out a dependency on the resource factory.
Another way to mock the Clusters service is this:
describe('Cluster Controller', function() {
var location, scope, controller, MockClusters, passPromise, q;
var cluster = {_id : '1'};
beforeEach(function(){
// since we are outside of angular.js framework,
// we inject the angujar.js services that we need later on
inject(function($rootScope, $controller, $q) {
scope = $rootScope.$new();
controller = $controller;
q = $q;
});
// let's mock the location service
location = {path: jasmine.createSpy('path')};
// let's mock the Clusters service
var MockClusters = function(){};
// since MockClusters is a function object (not literal object)
// we'll need to use the "prototype" property
// for adding methods to the object
MockClusters.prototype.$save = function(success, error) {
var deferred = q.defer();
var promise = deferred.promise;
// since the Clusters controller expect the result to be
// sent back as a callback, we register the success and
// error callbacks with the promise
promise.then(success, error);
// conditionally resolve the promise so we can test
// both paths
if(passPromise){
deferred.resolve(cluster);
} else {
deferred.reject();
}
}
// import the module containing the Clusters controller
module('mean.clusters')
// create an instance of the controller we unit test
// using the services we mocked (except scope)
controller('ClustersController', {
$scope: scope,
$location: location,
Clusters: MockClusters
});
it('save completes successfully', function() {
passPromise = true;
scope.save();
// since MockClusters.$save contains a promise (e.g. an async call)
// we tell angular to process this async call before we can validate
// the response
scope.$apply();
// we can call "toHaveBeenCalledWith" since we mocked "location.path" as a spy
expect(location.path).toHaveBeenCalledWith('clusters/' + cluster._id););
});
it('save doesn''t complete successfully', function() {
passPromise = false;
scope.save();
// since MockClusters.$save contains a promise (e.g. an async call)
// we tell angular to process this async call before we can validate
// the response
scope.$apply();
expect(location.path).toHaveBeenCalledWith('/error'););
});
});
});

Using spies in non testing code in angularjs

I recently dug a little deeper into unit testing. I was wondering if there is a way to use spies in production code as well. I've a tracking service. It would be nice to access other services and maybe even controllers, without haveing to alter their code.
Is there a way to spy on methods being called from services and controllers in the application code and what would be the best way to do so?
EDIT
Atm. I'm using this pattern for spying on services:
var vSetFNTrigger = function (sEvent, fnTrigger) {
fnTrigger.obj[fnTrigger.sMethod] = (function () {
var fnCached = fnTrigger.obj[fnTrigger.sMethod];
return function () {
$rootScope.$broadcast(sEvent, {});
return fnCached.apply(this, arguments);
};
})();
};
fnTrigger: {
obj: formData, // the service
sMethod: 'qPost' // the method to spy on
},
EDIT 2
I forgot to add a return to the inner function.
There should be nothing stopping you from doing this, although I think it is the wrong tool for the job.
If you are in Angular, you should consider using a decorator pattern. You can even use the provider decorator to intercept pretty much anything in Angular.
For instance, you might have a spy function that looks like this:
function createSpy(serviceName, source, spyNames, rootScope) {
var spy = angular.extend(angular.isFunction(source) ? function () {
console.log("Called " + serviceName + '()', arguments);
// broadcast with rootScope
return source.apply(source, arguments);
} : {}, source);
spyNames.forEach(function(name) {
var original = spy[name];
spy[name] = function() {
console.log("Called " + serviceName + '.' + name, arguments);
// broadcast with rootScope
return original.apply(spy, arguments);
};
});
return spy;
}
Then, you can create a generic function to generate a decorator:
function decorateWithSpy($provide, service, spyNames) {
$provide.decorator(service, function($delegate, $rootScope) {
return createSpy(service, $delegate, spyNames, $rootScope);
});
}
You can configure your spies like this:
app.config(function($provide) {
decorateWithSpy($provide, '$http', ['get']);
decorateWithSpy($provide, '$compile', []);
});
Doing this causes all of my $http and $compile functions to get printed to the console.

Categories