Testing async function with jasmine - javascript

We are facing an unexpected behavior while testing async code with Jasmine. As far as we know, when you are using the done function, expectations are not called until done is executed. But, that's not happening because the second expectation is failing, hence the $ctrl.todos assignment never happened
Not working test
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
// This method calls myService.getAll
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
expect($ctrl.todos).toEqual(false);
response.then(done);
});
Output: Expected undefined to equal false
On the other hand, this is working:
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
// This method calls myService.getAll
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
response
.then(() => expect($ctrl.todos).toBe(expected))
.then(done);
});
Output: test pass
Controller method:
$ctrl.$onInit = () => {
myService.getAll().then((data) => {
$ctrl.todos = data;
});
};

I found a solution. I don't really need another then. Specs with done will not complete until it's done is called. Also, it should be always placed after expectations.
Working code:
it('initializes the data when $onIinit', (done) => {
const expected = 'some result';
const response = Promise.resolve(expected);
spyOn(myService, 'getAll').and.returnValue(response);
$ctrl.$onInit();
expect(myService.getAll).toHaveBeenCalled();
response
.then(() => {
expect($ctrl.todos).toBe(expected);
done();
});
});

Your aproach seems to be correct, and probably calling done within afterEach will make it works.
afterEach(function(done) {
done();
}, 1000);
But, I would recommend using $httpBackend, the fake HTTP backend implementation suitable for unit testing applications that use the $http service. We are using angularjs anyway, right? So, why not take advantage of?
With $httpBackend we can make the requests, then response with mock data without really sending the request to a real server. Here an ilustrative example
it('initializes the data when $onIinit', () => {
const mockData = { data: { expected: 'some result' } };
spyOn(myService, 'getAll').and.callThrough();
$httpBackend.expect('POST', '/my-service/url').respond(200, mockData);
// This method calls myService.getAll
$ctrl.$onInit();
//flush pending request
$httpBackend.flush();
expect(myService.getAll).toHaveBeenCalled();
expect($ctrl.todos).toBeDefined();
});
Some explanations, $httpBackend.expect('POST', '/my-service/url'), here note that 'POST' need to match the method used by your service in myService.getAll, and '/my-service/url' is the url also used by your service in myService.getAll.
It is required to call $httpBackend.flush();, it will release all pending requests.
You will need to inject $httpBackend into your tests, an easy way would be
describe('$httpBackend service in module ngMock', () => {
let $httpBackend;
beforeEach(inject(['$httpBackend', (_$httpBackend) => {
$httpBackend = _$httpBackend;
}]));
it('$httpBackend is defined', () => {
// here we can use $httpBackend
expect($httpBackend).toBeDefined();
});
});
Also, note that $httpBackend is part of the ngMock module.
More info about testing angularjs code here
Hope it helps

Promise is from outside Angular world - you have to wait for result that will be available in next event queue tick - dirty (almost dirty) hack is to use setTimeout
angular.module('test', [])
.controller('underTest', function($scope, myService) {
$scope.$onInit = function() {
myService.getAll().then(function(data) {
$scope.todos = data
})
}
})
describe('Controller: somethingChanged', function() {
var scope, myService
beforeEach(function() {
module('test')
})
beforeEach(function() {
module(function($provide) {
$provide.value('myService', {
getAll: function() {}
})
})
})
beforeEach(inject(function($controller, _$rootScope_, _myService_) {
myService = _myService_
scope = _$rootScope_.$new()
$controller('underTest', {
$scope: scope
})
}))
it('initializes the data when $onIinit', function(done) {
const expected = 'some result'
const response = Promise.resolve(expected)
spyOn(myService, 'getAll').and.returnValue(response)
scope.$onInit()
expect(myService.getAll).toHaveBeenCalled();
setTimeout(function() {
expect(scope.todos).toEqual(expected)
done()
})
});
});
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine-html.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/2.6.1/boot.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/jasmine/2.6.1/jasmine.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-mocks.js"></script>

Related

Nock interceptors chaining, second mock ignored

A simple example of a mocking chain of requests with nock.
const request = require('request-promise');
module.exports = () => {
const URL1 = 'https://my.host.com/a/b/c/d';
const URL2 = 'https://my.host.com/a/b/x/y?k=v';
const options = {
method: 'POST',
uri: URL2,
body: {
some: 'payload'
},
json: true
};
return request(URL1)
.then(() => request(options))
.catch(e => console.error(e))
};
and test for it:
require('should');
const nock = require('nock');
const testFn = require('./');
describe('Check endpoint requests', () => {
beforeEach(() => {
nock.disableNetConnect();
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});
it('should hit correct endpoints', () => {
const scope = nock(`https://my.host.com/a/b`, {
encodedQueryParams: true,
})
.get('/c/d')
.reply(200)
.post('/x/y', {
some: 'payload'
})
.query({k: 'v'})
.reply(200);
testFn().then(() =>
scope.isDone().should.be.true()
);
});
});
As a result during the tests, the second "POST" request mock is completely ignored. After the hitting first mock URL1 - nock clearing the pending mocks for that scope and marks it as done.
The thing which counts I thing is that the basic URL is the same.
Is it a bug, or I use it incorrectly.
You have a few minor issues in your test.
First, the value passed to nock should just be the origin and shouldn't include part of the path. Instead, in your case, get and post should have the full path.
Second, you want to remove encodedQueryParams: true. That flag means the interceptor is created using already encoded query/search params, however, you're calling it like .query({k: 'v'}), which is not pre-encoded.
The last issue is that you weren't telling Mocha when the test was finished. So it was completing the test before having all of its results. There are two ways to achieve this. Either accept an argument in the it callback, done is the nomenclature. Or make the callback async and await your requests. I've implemented the latter below.
it('should hit correct endpoints', async () => {
const scope = nock('https://my.host.com')
.get('/a/b/c/d')
.reply(200)
.post('/a/b/x/y', {
some: 'payload'
})
.query({k: 'v'})
.reply(200);
await testFn();
scope.isDone().should.be.true();
});

How to assert stubbed fetch more than once

Using proxyquire, sinon, and mocha.
I am able to stub fetch on the first call of fetch. But on the second fetch call, which is recursive, I am not able to assert it. From the output, it looks like the assertion may run before the test finishes. You will see this with second fetch console out after assertion.
index.js
var fetch = require('node-fetch');
function a() {
console.log('function a runs');
fetch('https://www.google.com')
.then((e) => {
console.log('first fetch');
b();
})
.catch((e)=> {
console.log('error')
});
}
function b() {
fetch('https://www.google.com')
.then((e) => {
console.log('second fetch');
})
.catch((e)=> {
console.log('error')
});
}
a()
test:
describe('fetch test demo', ()=> {
it('fetch should of called twice', (done)=> {
fetchStub = sinon.stub();
fetchStub2 = sinon.stub();
fetch = sinon.stub();
fetchStub.returns(Promise.resolve('hello'));
fetchStub2.returns(Promise.resolve('hi'));
var promises = [ fetchStub, fetchStub2 ]
fetch.returns(Promise.all(promises));
proxy('../index', {
'node-fetch': fetch
});
fetch.should.have.been.callCount(2);
done()
});
});
fetch test demo
function a runs
1) fetch should of called twice
first fetch
second fetch
lifx alert test
- fetch should of called three times
when rain change is over 50%
- should run fetch twice
0 passing (78ms)
2 pending
1 failing
1) fetch test demo fetch should of called twice:
expected stub to have been called exactly twice, but it was called once
stub(https://www.google.com) => [Promise] { } at a (/home/one/github/lifx-weather/foobar.js:5:3)
AssertionError: expected stub to have been called exactly twice, but it was called once
stub(https://www.google.com) => [Promise] { } at a (foobar.js:5:3)
at Context.it (test/bar.js:22:28)
Updated version
#dman, since you updated your test case I owe you an updated answer. Although rephrased, the scenario is still unorthodox - it seems like you want to ignore in a sense the 'law of gravity' even though you know it's right there in front of you.
I'll try to be as descriptive as possible. You have two functions which are doing async stuff by design. a() calls b() sequentially - by the way this is not recursion. Both functions do not notify their callers upon completion / failure, i.e. they are treated as fire-and-forget.
Now, let's have a look at your test scenario. You create 3 stubs. Two of them resolve to a string and one combining their execution using Promise.all(). Next, you proxy the 'node-fetch' module
proxy('./updated', {
'node-fetch': fetch
});
using the stub that returns the combined execution of stubs 1 & 2. Now, if you print out the resolved value of fetch in either function, you will see that instead of a string it's an array of stubs.
function a () {
console.log('function a runs');
fetch('http://localhost')
.then((e) => {
console.log('first fetch', e);
b();
})
.catch((e) => {
console.log('error');
});
}
Which I guess is not the intended output. But let's move over as this is not killing your test anyway. Next, you have added the assertion together with the done() statement.
fetch.should.have.been.callCount(2);
done();
The issue here is that whether you are using done() or not, the effect would be exactly the same. You are executing your scenario in sync mode. Of course in this case, the assertion will always fail. But the important thing here is to understand why.
So, let's rewrite your scenario to mimic the async nature of the behavior you want to validate.
'use strict';
const chai = require('chai');
const sinon = require('sinon');
const SinonChai = require('sinon-chai');
chai.use(SinonChai);
chai.should();
const proxy = require('proxyquire');
describe('fetch test demo', () => {
it('fetch should of called twice', (done) => {
var fetchStub = sinon.stub();
var fetchStub2 = sinon.stub();
var fetch = sinon.stub();
fetchStub.returns(Promise.resolve('hello'));
fetchStub2.returns(Promise.resolve('hi'));
var promises = [fetchStub, fetchStub2];
fetch.returns(Promise.all(promises));
proxy('./updated', {
'node-fetch': fetch
});
setTimeout(() => {
fetch.should.have.been.callCount(2);
done();
}, 10);
});
});
As you can see, the only change made was wrapping the assertion within a timer block. Nothing much - just wait for 10ms and then assert. Now the test passes as expected. Why?
Well, to me it's pretty straightforward. You want to test 2 sequentially executed async functions and still run your assertions in sync mode. That sounds cool, but it's not gonna happen :) So you have 2 options:
Have your functions notify callers upon completion and then run your assertions in truly async mode
Mimic the async nature of things using unorthodox techniques
Reply based on original test scenario
It can be done. I've re-factored your provided files a bit so that
can be executed.
index.js
const fetch = require('node-fetch');
const sendAlert = require('./alerts').sendAlert;
module.exports.init = function () {
return new Promise((resolve, reject) => {
fetch('https://localhost')
.then(function () {
sendAlert().then(() => {
resolve();
}).catch(
e => reject(e)
);
})
.catch(e => {
reject(e);
});
});
};
alerts.js
const fetch = require('node-fetch');
module.exports.sendAlert = function () {
return new Promise((resolve, reject) => {
fetch('https://localhost')
.then(function () {
resolve();
}).catch((e) => {
reject(e);
});
});
};
test.js
'use strict';
const chai = require('chai');
const sinon = require('sinon');
const SinonChai = require('sinon-chai');
chai.use(SinonChai);
chai.should();
const proxy = require('proxyquire');
describe.only('lifx alert test', () => {
it('fetch should of called twice', (done) => {
var body = {
'hourly': {
data: [{
time: 1493413200,
icon: 'clear-day',
precipIntensity: 0,
precipProbability: 0,
ozone: 297.17
}]
}
};
var response = {
json: () => {
return body;
}
};
const fetchStub = sinon.stub();
fetchStub.returns(Promise.resolve(response));
fetchStub['#global'] = true;
var stubs = {
'node-fetch': fetchStub
};
const p1 = proxy('./index', stubs);
p1.init().then(() => {
try {
fetchStub.should.have.been.calledTwice;
done();
} catch (e) {
done(e);
}
}).catch((e) => done(e));
});
});
What you're trying to do though is a bit unorthodox when it comes to
good unit testing practices. Although proxyquire supports this
mode of stubbing through a feature called global overrides, it is
explained here why should anyone think twice before going down
this path.
In order to make your example pass the test, you just need to add an
extra attribute to the Sinon stub called #global and set it to
true. This flag overrides the require() caching mechanism and
uses the provided stub no matter which module is called from.
So, although what you're asking can be done I will have to agree with
the users that commented your question, that this should not be
adopted as a proper way of structuring your tests.
Here is also a alternative way to do this using Promise.all().
Note: this won't work if using fetch's json method and you need to pass data in the resolve() for logic on data. It will only pass in the stubs when resolved. However, it will assert the number of times called.
describe('fetch test demo', () => {
it('fetch should of called twice', () => {
let fetchStub = sinon.stub();
let fetchStub2 = sinon.stub();
let fetch = sinon.stub();
fetchStub.returns(Promise.resolve('hello'));
fetchStub2.returns(Promise.resolve('hi'));
var promises = [ fetchStub, fetchStub2 ]
var promise = Promise.all(promises);
fetch.returns(promise);
proxy('../foobar', { 'node-fetch': fetch });
return promise.then(() => {
fetch.should.have.callCount(2);
});
});
});
I have found another way to get things done.
May be this could work for someone.
describe('Parent', () => {
let array: any = [];
before(async () => {
array = await someAsyncDataFetchFunction();
asyncTests();
});
it('Dummy test to run before()',async () => {
expect(0).to.equal(0); // You can use this test to getting confirm whether data fetch is completed or not.
});
function asyncTests() {
array.forEach((currentValue: any) => {
describe('Child', async () => {
it('Test '+ currentValue ,() => {
expect(currentValue).to.equal(true);
})
})
});
}
});
That's how I achieved the assertion on every element of the array. (Array data is being fetch asynchronously).

Unit Testing: Karma-Jasmine not resolving Promise without implicitly calling $rootScope.$digest();

I have an Angular service that makes a call to the server and fetch the user list. The service returns a Promise.
Problem
Promise is not being resolved until and unless I call $rootScope.$digest(); either in the service or in the test itself.
setTimeout(function () {
rootScope.$digest();
}, 5000);
Apparently, Calling $rootScope.$digest(); is a workaround and I cannot call it in the angular service so I am calling it in the unit test with an interval of 5 seconds which I think is a bad practice.
Request
Please suggest an actual solution for this.
Given below is the test that I have written.
// Before each test set our injected Users factory (_Users_) to our local Users variable
beforeEach(inject(function (_Users_, $rootScope) {
Users = _Users_;
rootScope = $rootScope;
}));
/// test getUserAsync function
describe('getting user list async', function () {
// A simple test to verify the method getUserAsync exists
it('should exist', function () {
expect(Users.getUserAsync).toBeDefined();
});
// A test to verify that calling getUserAsync() returns the array of users we hard-coded above
it('should return a list of users async', function (done) {
Users.getUserAsync().then(function (data) {
expect(data).toEqual(userList);
done();
}, function (error) {
expect(error).toEqual(null);
console.log(error.statusText);
done();
});
///WORK AROUND
setTimeout(function () {
rootScope.$digest();
}, 5000);
});
})
service
Users.getUserAsync = function () {
var defered = $q.defer();
$http({
method: 'GET',
url: baseUrl + '/users'
}).then(function (response) {
defered.resolve(response);
}, function (response) {
defered.reject(response);
});
return defered.promise;
}
You can cause the promises to flush with a call to $timeout.flush(). It makes your tests a little bit more synchronous.
Here's an example:
it('should return a list of users async', function (done) {
Users.getUserAsync().then(function (data) {
expect(data).toEqual(userList);
done();
}, function (error) {
expect(error).toEqual(null);
console.log(error.statusText);
done();
});
$timeout.flush();
});
Aside: the failback won't be handled so it adds additional complexity to the test.

Angular unit tests, mocking a service that is injected using $injector

I'm trying to unit test an Angular service, that has another service injected into it using $injector, rather than passing the service dependency as an argument. The injected service is dynamic - it could be one of a number of different services - and so it can't be passed as a function argument.
The service under test determines if the application is online or offline, then uses the strategy pattern to either make an HTTP call (we're online, so the strategy is 'remote') or use local storage to get the required data (we're offline, so the strategy is 'local'). It's used in a number of scenarios across the application, so the service and the service method that is subsequently called are dynamic. Easier to show the simplified, relevant, code:
class NetworkStrategyService {
constructor (private Offline, private $injector) {}
fulfill (config) {
this.Offline.check()
.then(
() => {
// we're online
const service = $injector.get(config.remote.service);
service[config.remote.method](config.remote.data)
.then(() => {
// Do something with the data
})
},
() => {
// Grab it from local storage
}
)
}
}
My problem is, because I can't inject the service in the normal way in my unit tests, I don't know how to test this code. I've mocked the config, now I want to test that the correct service methods are being called while online and offline, such as:
it (`calls the remote service, with the remote method, if we're online`, () => {
const remoteMethod = spyOn(config.remote.service, config.remote.method);
// Set up, to get a successful Offline.check() outcome
expect(remoteMethod).toHaveBeenCalledWith(config.remote.data);
});
How can I ensure that the mocked service is a service, and pass it into my service under test?
OK, I seem to have solved it:
let deferred;
let config = {
remote: {
service: 'RemoteService',
method: 'remoteAction',
data: {alpha:1}
},
local: ...
};
beforeEach(() => {
angular.mock.module(($provide) => {
$provide.service(config.remote.service, () => {
return {
[config.remote.method]: () => {}
};
});
});
});
beforeEach(() => {
inject((_$q_, _$rootScope_, _$injector_) => {
$q = _$q_;
$scope = _$rootScope_.$new();
$injector = _$injector_
});
deferred = $q.defer();
});
beforeEach(() => serviceUnderTest = new NetworkStrategyService(Offline, $injector));
it (`calls the remote service, with the remote method, if we're online`, () => {
let dynamicService;
inject((_RemoteService_) => {
dynamicService = _RemoteService_;
});
serviceUnderTest.strategy = 'remote';
const dynamicMethod = spyOn(dynamicService, config.remote.method).and.returnValue(deferred.promise);
serviceUnderTest.makeCall(config);
deferred.resolve();
$scope.$apply();
expect(dynamicMethod).toHaveBeenCalledWith(config.remote.data);
});

Jasmine Testing with Angular Promise not resolving with TypeScript

I have a fairly straightforward test that works against an Angular promise, which I'm resolving in the beforeEach function, but the then in my code is not ever firing and I can't see what I'm missing. These are written with TypeScript, but that doesn't really have any bearing on the problem.
Here is my test
describe('Refresh->', () => {
var controller = new Directives.Reporting.ReportDirectiveController($scope, $q, $location);
var called = false;
var defer: any;
beforeEach((done) => {
controller.drillReport = (drillReport: Models.drillReport): ng.IPromise<Models.drillData> => {
defer = $q.defer();
called = true;
defer.resolve({});
return defer.promise;
};
spyOn(controller, 'processResults');
controller.refresh();
done();
});
it('Calls DrillReport', () => {
expect(called).toBeTruthy();
});
it('Calls ProcessResults', () => {
expect(controller.processResults).toHaveBeenCalled();
});
});
The Refresh method in the controller looks like this:
refresh() {
this.drillReport({ drillReport: drillReport })
.then((results: Models.drillData) => {
parent.processResults(results, parent.availableDrills, this.columns, this.gridOptions, undefined, undefined);
});
}
What you are missing is that you will need access to use $scope, or $rootScope, so that you can call and force a digest cycle...
$scope.$digest();
The reason this is needed is that the resolved and rejected promises are processed during the digest loop. So while you are resolving the promise in your mock, the actual promise callback is not being invoked.
Following up on what #Brocco said, your code is calling done() before the promise is processed.
What you need it a way for your test code to know that that parent.processResults() has been called.
I suggest you have refresh return a promise that will be resolved just after parent.processResults(), and add controller.refresh().finally(() => { done(); }); in your test code.

Categories