Unit test a function that ends the promise chain - javascript

Let's say I have a function in a class named UserController that does something along those lines (where userService.createUser() returns a promise):
function createUser(req, res)
{
const userInfo = req.body;
userService.createUser(userInfo)
.then(function(){res.json({message: "User added successfully"})})
.fail(function(error){res.send(error)})
.done();
}
How can I test that, when the promise resolves, res.json() is called, and when the promise rejects, res.send(error) is called?
I have tried writing a test like this:
const userService = ...
const userController = new UserController(userService);
const response = {send: sinon.stub()};
...
const anError = new Error();
userService.createUser = sinon.stub().returns(Q.reject(anError));
userController.createUser(request, response);
expect(response.send).to.be.calledWith(anError);
But the test fails with "response.send is never called". I also tried logging something before calling res.send(error) and the logging does happen.
My guess is that expect() is called before res.send(error) is executed since it's asynchronous.
I'm fairly new with promises and unit tests, is it something with my architecture or my use of promises?
I'm using Q for promises and mocha, chai, sinon for my unit tests.

As you have an asynchronous call the expect statement is called right after userController.createUser() line. So when the assertion is evaluated it has not been called yet.
To test your code asynchronously you will need to declare done on your it statement and then call it manually to get the result.
On your test file:
it('should work', function(done) {
...
userController.createUser(request, response);
process.nextTick(function(){
expect(response.send).to.be.calledWith(anError);
done();
});
});
This will make Mocha (I'm assuming you are using it) evaluate your excpect just when done() is called.
Alternatively you could set a cb function on your UserController.createUser function and call it on .done():
UserController
function createUser(req, res, cb) {
const userInfo = req.body;
userService.createUser(userInfo)
.then(function(){res.json({message: "User added successfully"})})
.fail(function(error){res.send(error)})
.done(function(){ if(cb) cb() });
}
And then on your test:
userController.createUser(request, response, function() {
expect(response.send).to.be.calledWith(anError);
done();
});

The easier way, assuming you use Mocha or Jasmine as the framework, is to go ahead as you first started, but just skip Sinon completely (as it is not needed here, unless you test for actual arguments received):
// observe the `done` callback - calling it signals success
it('should call send on successful service calls', (done) => {
// assuming same code as in question
...
const response = {send: done};
userController.createUser(request, response);
});
// observe the `done` callback - calling it signals success
it('should call send on failing service calls', (done) => {
// assuming same code as in question
...
const response = {send: err => err? done(): done(new Error("No error received"))};
userController.createUser(request, response);
});
Disclosure: I am part of the Sinon maintainer team.

Related

Mocha/Chai/Supertest passing tests when not meeting requirements [duplicate]

I'm using Mocha and SuperTest to test my Express API. However my first test always seems to pass when inside the .then() of my request().
I'm passing in a String to a test that is expecting an Array. So should definitely fail the test.
It fails outside of the then() as expected, but I won't have access to the res.body there to perform my tests.
Here is my code:
const expect = require('chai').expect;
const request = require('supertest');
const router = require('../../routes/api/playlist.route');
const app = require('../../app');
describe('Playlist Route', function() {
// before((done) => {
// }
describe('Get all playlists by user', function() {
it('Should error out with "No playlists found" if there are no Playlists', function() {
request(app).get('/api/playlists/all')
.then(res => {
const { body } = res;
// Test passes if expect here
expect('sdfb').to.be.an('array');
})
.catch(err => {
console.log('err: ', err);
});
// Test fails if expect here
expect('sdfb').to.be.an('array');
})
})
});
I found this article but I'm not using a try catch block, but I thought maybe it could have something to do with the promise.
Quick reponse
it('decription', function(done) {
asyncFunc()
.then(() => {
expect(something).to.be(somethingElse)
done()
})
})
Detailed response in the comment of #jonrsharpe
Rather than using done, simply return request(app).get('/api/playlists/all') since request() returns a promise. Since you have expect('sdfb').to.be.an('array'); twice, remove the one that's not in the .then callback. When using asynchronous code, remember that synchronous code that appears to come after the async chain will execute before the promise .then handlers. This is counterintuitive.
Here's the .then approach:
it('should ...', () => {
return request(app)
.get('/api/playlists/all')
.then(res => {
const {body} = res;
// assert here
});
});
The other approach is to await the promise yourself in the test case function, then make assertions on the resolved response object. In this case, drop the then chain. This approach is generally preferred as it reduces nesting.
it('should ...', async () => {
const res = await request(app).get('/api/playlists/all');
const {body} = res;
// assert here
});
If you don't let Mocha know you're working with asynchronous code by returning a promise, awaiting the promises, or adding and calling the done parameter, the assertions occur asynchronously after the test is over and disappear into the void, creating a false positive.
Skip .catch either way. Since you've informed Mocha of the promise, if it rejects, it'll let you know.

How to throw an error from an async mongoose middleware post hook

What's the correct way to throw an error from an async Mongoose middleware post hook?
Code Example
The following TypeScript code uses mongoose's post init event to run some checks that are triggered whenever a function retrieves a doc from mongoDb. The postInit() function in this example is executing some background checks. It is supposed to fail under certain circumstances and then returns a Promise.reject('Error!');
schema.post('init', function (this: Query<any>, doc: any) {
return instance.postInit(this, doc)
.catch( err => {
return err;
});
});
The hook works fine. I.e. the following code triggers the hook:
MyMongooseModel.findOne({ _id : doc.id}, (err, o : any) => {
console.log(o);
});
However, if postInit() fails, the error isn't passed back to the calling function. Instead, the document is returned.
Expected behavior
I'm looking for the right way to pass this error to the calling function. If the background checks fail, the calling function shouldn't get a document back.
I have tried different ways to raise this error. E.g. throw new Error('Error');. However, this causes an UnhandledPromiseRejectionWarning and still returns the document.
Mongoose maintainer here. Unfortunately, init() hooks are synchronous and we haven't done a good job documenting that. We opened up a GitHub issue and will add docs on that ASAP. The only way to report an error in post('init') is to throw it.
const assert = require('assert');
const mongoose = require('mongoose');
mongoose.set('debug', true);
const GITHUB_ISSUE = `init`;
const connectionString = `mongodb://localhost:27017/${ GITHUB_ISSUE }`;
const { Schema } = mongoose;
run().then(() => console.log('done')).catch(error => console.error(error.stack));
async function run() {
await mongoose.connect(connectionString);
await mongoose.connection.dropDatabase();
const schema = new mongoose.Schema({
name: String
});
schema.post('init', () => { throw new Error('Oops!'); });
const M = mongoose.model('Test', schema);
await M.create({ name: 'foo' });
await M.findOne(); // Throws "Oops!"
}
This is because Mongoose assumes init() is synchronous internally.
In this post init hook method you only receive a doc:
Document.prototype.init()
Parameters doc «Object» document returned by
mongo Initializes the document without setters or marking anything
modified.
Called internally after a document is returned from mongodb.
Mongoose Documentation: Init HookDocumentation
And for trigger a error you need a done or next method:
Post middleware
post middleware are executed after the hooked method and all of its
pre middleware have completed. post middleware do not directly receive
flow control, e.g. no next or done callbacks are passed to it. post
hooks are a way to register traditional event listeners for these
methods.
Mongoose Documentation: Post Middleware
If you only want to know if happened a error in your call, change for this:
MyMongooseModel.findOne({ _id : doc.id}, (err, o : any) => {
if(err) {
throw new Error(err);
}
console.log(o);
});
If you want to propagate the error one option is use a pre hook method:
schema.pre('save', function(next) {
const err = new Error('something went wrong');
// If you call `next()` with an argument, that argument is assumed to be
// an error.
next(err);
});
schema.pre('save', function() {
// You can also return a promise that rejects
return new Promise((resolve, reject) => {
reject(new Error('something went wrong'));
});
});
schema.pre('save', function() {
// You can also throw a synchronous error
throw new Error('something went wrong');
});
schema.pre('save', async function() {
await Promise.resolve();
// You can also throw an error in an `async` function
throw new Error('something went wrong');
});
Example Error Handling: Error Handling

How to write a test using Mocha+Chai to expect an exception from setTimeout?

I have following:
it('invalid use', () => {
Matcher(1).case(1, () => {});
});
The case method is supposed to throw after some delay, how can I describe it for Mocha/Chai that's what I want - the test should pass (and must not pass when exception is not thrown)?
Consider case method off limits, it cannot be changed.
For testing purposes it should be equivalent to:
it('setTimeout throw', _ => {
setTimeout(() => { throw new Error(); }, 1); // this is given, cannot be modified
});
I tried:
it('invalid use', done => {
Matcher(1).case(1, () => {});
// calls done callback after 'case' may throw
setTimeout(() => done(), MatcherConfig.execCheckTimeout + 10);
});
But that's not really helping me, because the test behavior is exactly reverted - when an exception from case (setTimeout) is not thrown, it passes (should fail) and when an exception is thrown the test fails (should succeed).
I read somewhere someone mentioning global error handler, but I would like to solve this cleanly using Mocha and/or Chai, if it is possible (I guess Mocha is already using it in some way).
You cannot handle exceptions from within a asynchronous callback, e.g. see Handle error from setTimeout. This has to do with the execution model ECMAScript uses. I suppose the only way to catch it is in fact to employ some environment-specific global error handling, e.g. process.on('uncaughtException', ...) in Node.js.
If you convert your function to Promises, however, you can easily test it using the Chai plugin chai-as-promsied:
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const expect = chai.expect;
it('invalid use', async () => {
await expect(Matcher(1).case(1, () => {})).to.eventually.be.rejected;
});
Any Mocha statements like before, after or it will work asynchronously if you return a promise. I generally use something like the below for async tests.
Also don't forget to set timeout this.timeout(...) if you expect the async function to take more than 2 seconds.
it('some test', () => {
return new Promise(function(resolve,reject){
SomeAsyncFunction(function(error,vals) {
if(error) {
return reject(error);
} else {
try {
//do some chai tests here
} catch(e) {
return reject(e);
}
return resolve();
}
});
});
});
Specifically for your case, since we expect some error to be thrown after a period of time (assuming the empty callback you have provided to .case should not run due to the exception being thrown) then you can write it something like:
it('invalid use', () => {
//define the promise to run the async function
let prom = new Promise(function(resolve,reject){
//reject the promise if the function does not throw an error
//note I am assuming that the callback won't run if the error is thrown
//also note this error will be passed to prom.catch so need to do some test to make sure it's not the error you are looking for.
Matcher(1).case(1, () => {return reject(new Error('did not throw'))});
});
prom.catch(function(err){
try {
expect(err).to.be.an('error');
expect(err.message).to.not.equal('did not throw');
//more checks to see if err is the error you are looking for
} catch(e) {
//err was not the error you were looking for
return Promise.reject(e);
}
//tests passed
return Promise.resolve();
});
//since it() receives a promise as a return value it will pass or fail the test based on the promise.
return prom;
});
From Chai documentation :
When no arguments are provided, .throw invokes the target function and asserts that an error is thrown.
So you could something like
expect(Matcher(1).case(1, () => {})).to.throw
If your tested code calls setTimeout with a callback that throws and no-one is catching this is exception then:
1) this code is broken
2) the only way to see that problem is platform global exception handler like process.on('uncaughtException' mentioned by user ComFreek
The last resort chance is to stub setTimeout for duration of test (for example using sinon.stub) or just manually.
In such stubbed setTimeout you can decorate timeout handler, detect exception and call appropriate asserts.
NOTE, this is last resort solution - your app code is broken and should be fixed to properly propagate errors, not only for testing but ... well, to be good code.
Pseudocode example:
it('test', (done) => {
const originalSetTimeout = setTimeout;
setTimeout = (callback, timeout) => {
originalSetTimeout(() => {
try {
callback();
} catch(error) {
// CONGRATS, you've intercepted exception
// in _SOME_ setTimeout handler
}
}, timeout)
}
yourTestCodeThatTriggersErrorInSomeSetTimeoutCallback(done);
})
NOTE2: I intentionally didn't wrote proper async cleanup code, it's a homework. Again, see sinon.js and its sandbox
NOTE3: It will catch all setTimeout calls during test duration. Beware, there are dragons.

Can't test effects of resolved promise

I have the following code in user:
import { redirectTo } from 'urlUtils';
export function create(user) {
api.postUser(user).then((response) => {
redirectTo(response.userUrl);
})
}
And I have the following test:
import * as api from 'api'
import * as user from 'user'
sinon.stub(api, 'postUser').returns(
Promise.resolve({ userUrl: 'a-url' })
);
sinon.spy(urlUtils, 'redirectTo');
const userData = {id: 2};
user.create(userData);
expect(api.postUser.calledWith(userData)).toBe(true); // This passes
expect(urlUtils.redirectTo.calledOnce).toBe(true); // This fails
I have been able to test it on the browser, and the redirect is happening. What am I missing here? I have stubbed the request call to resolve the promise synchronously, so that shouldn't be a problem.
Promises are asynchronous, so when you're doing expect(urlUtils.redirectTo.calledOnce).toBe(true) the promise hasn't been resolved yet.
The easiest way to get around it is to wrap that expectation in a short timeout and then use whatever utility the testing framework you're using provides to signal that an asynchronous test is complete. Something like this:
setTimeout(() => {
expect(urlUtils.redirectTo.calledOnce).toBe(true);
done();
}, 5);
Another, nicer solution is to actually use the promise that your stub returns. First, keep a reference to that promise:
Replace:
sinon.stub(api, 'postUser').returns(
Promise.resolve({ userUrl: 'a-url' })
);
with:
const postUserPromise = Promise.resolve({ userUrl: 'a-url' });
sinon.stub(api, 'postUser').returns(postUserPromise);
then write your expectation like this:
postUserPromise.then(() => {
expect(urlUtils.redirectTo.calledOnce).toBe(true);
done();
});
done() is the function most test frameworks (at least Jasmine and Mocha as far as I know) provide to signal that an asynchronous test is completed. You get it as the first argument to the function your test is defined in and by specifying it in your function signature you're telling the test framework that your test is asynchronous.
Examples:
it("is a synchronous test, completed when test function returns", () => {
expect(true).to.equal(true);
});
it("is an asynchronous test, done() must be called for it to complete", (done) => {
setTimeout(() => {
expect(true).to.equal(true);
done();
}, 5000);
});

Node.js Q promises then() chaining not waiting

I am trying to refactor the following code to avoid the callback hell, transforming it into:
createUser(user_data)
.then(createClient(client_data))
.then(createClientPartner(clientpartner_data))
.then(function(data) {
cb(null, _.pick(data,['id','username']));
}, function(error) {
cb(error);
});
As you see, I created a method for each one of the steps:
function createUser(user_data) {
console.log('createUser');
var deferred = Q.defer()
new db.models.User.create(user_data, function(err, user) {
console.log('createUser done');
if (!!err)
deferred.reject(err);
else {
client_data['id'] = user.id;
deferred.resolve(user);
}
});
return deferred.promise;
}
The other methods have identical console.log calls to be able to follow the execution path.
I would expect it to be:
createUser
createUser done
createClient
createClient done
createClientPartner
createClientPartner done
But instead, it is:
createUser
createClient
createClientPartner
createClientPartner done
createUser done
createClient done
Why does are the functions triggered when the previous promise have not been resolved? I expect "then" to wait until previous promise have been resolved or rejected to continue. Am I missing something important about promises?
The problem is you don't pass functions, but the result of function calls.
Instead of
createUser(user_data)
.then(createClient(client_data))
you should have
createUser(user_data)
.then(function(user){
createClient(client_data) // no need for the user ? really ?
})

Categories

Resources