how to assert catch promise with sinon and chai - javascript

We have a method in our CLI which uses method returning a promise to print message to user.
exports.handler = (argv) => {
let customUtils = new Utils(argv);
Utils.deploy()
.then(res => console.log(`Ressource was deployed`))
.catch(e => {
console.error(`Ressource was not deployed`);
console.error(e);
process.exit(1);
});
}
We are looking for a way to test console errors and process exit in case of deploy() promise rejection.
We tried using sandbox stub then assert in an async test:
describe('when promise is errored', () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
utilsStub = sandbox.stub(Utils.prototype, 'deploy').rejects('rejected');
processStub = sandbox.stub(process, 'exit');
consoleStub = sandbox.stub(console, 'error');
});
afterEach(() => {
sandbox.restore();
});
it('should call deploy and log the error before exiting', async () => {
await handler({});
expect(utilsStub).to.have.been.called;
expect(console.error).to.have.been.called;
});
});
This test doesn't work: AssertionError: expected error to have been called at least once, but it was never called.
The same happens when we expect(process.exit).to.have.been.called;. It's never called.
We successfuly tested the then part in a similary way:
describe('when promise is resolved', () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
utilsStub = sandbox.stub(Utils.prototype, 'deploy').callsFake(() => Promise.resolve('some text'));
consoleStub = sandbox.stub(console, 'log');
});
afterEach(() => {
sandbox.restore();
});
it('should call deploy and print success message', async () => {
await handler({});
expect(utilsStub).to.have.been.called;
expect(console.log).to.have.been.calledWith('Ressource was deployed');
});
});

There are some things to fix the source and test file.
For source file, we must use customUtils to call deploy() function. Since, you can use async/await, convert it from Promise can produce better code.
exports.handler = async argv => { // put async
let customUtils = new Utils(argv);
try {
await customUtils.deploy(); // change to await and use customUtils
console.log(`Ressource was deployed`);
} catch (e) {
console.error(`Ressource was not deployed`);
console.error(e);
process.exit(1);
}
};
For test file, nothing changes
describe('when promise is errored', () => {
beforeEach(() => {
sandbox = sinon.createSandbox();
utilsStub = sandbox.stub(Utils.prototype, 'deploy').rejects('rejected');
processStub = sandbox.stub(process, 'exit');
consoleStub = sandbox.stub(console, 'error');
});
afterEach(() => {
sandbox.restore();
});
it('should call deploy and log the error before exiting', async () => {
await handler({});
expect(utilsStub).to.have.been.called;
expect(console.error).to.have.been.called;
expect(process.exit).to.have.been.called; // add it
});
});
UPDATED:
In case want to still use promise, we have to make sure we return the promise.
exports.handler = (argv) => {
let customUtils = new Utils(argv);
return customUtils.deploy() // <== specify return here
.then(res => console.log(`Ressource was deployed`))
.catch(e => {
console.error(`Ressource was not deployed`);
console.error(e);
process.exit(1);
});
};
Hope it helps

You need to be able await the result of exports.handler before you test your assertions. You are awaiting it, but exports.handler is not returning the promise, so there's nothing to await in the test — exports.handler returns undefined immediately so the test runs the assertions in the same event loop before console.error can be called.
I'm not sure why you aren't seeing similar problems in the test where the promise resolves. (Maybe worth checking that that test fails properly)
This should help:
exports.handler = (argv) => {
let customUtils = new Utils(argv);
//Utils.deploy() // <- is that a typo?
return customUtils.deploy()
.then(res => console.log(`Ressource was deployed`))
.catch(e => {
console.error(`Ressource was not deployed`);
console.error(e);
process.exit(1);
});
}
Also in your tests you are creating a spy with:
consoleStub = sandbox.stub(console, 'error');
But writing the assertion directly on console.error. I don't think this should work:
expect(console.error).to.have.been.called;
// maybe expect(consoleStub)...
With those changes the test passes for me and (more importantly) fails when I don't call console.error in the catch.

Related

Emitting an error in my jest test doesn't trigger the error handler as expected

My download code relies on listening on listening for events to determine when to fire callbacks, and whether the promise it's in should be resolved or rejected:
async function downloadMtgJsonZip() {
const path = Path.resolve(__dirname, 'resources', fileName);
const writer = Fs.createWriteStream(path);
console.info('...connecting...');
const { data, headers } = await axios({
url,
method: 'GET',
responseType: 'stream',
});
return new Promise((resolve, reject) => {
const timeout = 20000;
const timer = setTimeout(() => {
console.log('timed out'); // debug log
writer.close();
reject(new Error(`Promise timed out after ${timeout} ms`));
}, timeout);
let error = null;
const totalLength = headers['content-length'];
const progressBar = getProgressBar(totalLength);
console.info('...starting download...');
// set up data and writer listeners
data.on('data', (chunk) => progressBar.tick(chunk.length));
data.on('error', (err) => { // added this to see if it would be triggered - it is not
console.log(`did a data error: ${error}`);
error = err;
clearTimeout(timer);
writer.close();
reject(err);
});
writer.on('error', (err) => {
console.log(`did a writer error: ${error}`);
error = err;
clearTimeout(timer);
writer.close();
reject(err);
});
writer.on('close', () => {
const now = new Date();
console.log(`close called: ${now}`);
console.log(`error is: ${error}`);
console.info(
`Completed in ${(now.getTime() - progressBar.start) / 1000} seconds`,
);
clearTimeout(timer);
console.log(`time cleared: ${timer}`);
if (!error) resolve(true);
// no need to call the reject here, as it will have been called in the
// 'error' stream;
});
// finally call data.pipe with our writer
data.pipe(writer);
});
}
I had some issues writing my tests, but I managed to get something that worked, despite feeling slightly messy, based on this advice:
Here is my test, with the relevant bits of my set up:
describe('fetchData', () => {
let dataChunkFn;
let dataErrorFn;
let dataOnFn;
let writerCloseFn;
let writerErrorFn;
let writerOnFn;
let pipeHandler;
beforeEach(() => {
// I've left all the mocking in place,
// to give an idea of what I've set up
const mockWriterEventHandlers = {};
const mockDataEventHandlers = {};
dataChunkFn = jest.fn((chunk) => mockDataEventHandlers.data(chunk));
dataErrorFn = jest.fn((chunk) => mockDataEventHandlers.data(chunk));
dataOnFn = jest.fn((e, cb) => {
mockDataEventHandlers[e] = cb;
});
writerCloseFn = jest.fn(() => mockWriterEventHandlers.close());
writerErrorFn = jest.fn(() => mockWriterEventHandlers.error());
writerOnFn = jest.fn((e, cb) => {
mockWriterEventHandlers[e] = cb;
});
const getMockData = (pipe) => ({
status: 200,
data: {
pipe,
on: dataOnFn,
},
headers: { 'content-length': 100 },
});
axios.mockImplementationOnce(() => getMockData(pipeHandler));
fs.createWriteStream.mockImplementationOnce(() => ({
on: writerOnFn,
close: writerCloseFn,
}));
jest.spyOn(console, 'info').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {});
});
it.only('handles errors from the writer', async (done) => {
console.log('writer error');
expect.assertions(1);
pipeHandler = (writer) => writer.emit('error', new Error('bang'));
try {
await downloadMtgJsonZip();
done.fail('ran without error');
} catch (exception) {
// expect(dataErrorFn).toHaveBeenCalled(); // neither of these are called
expect(writerErrorFn).toHaveBeenCalled();
}
});
I would have expected, that when data(pipe) ran, and the writer emitted a new error, it would have triggered at least one of the error listeners.
The code runs as expected, and it even handles the timeout (which I initially set too low), but this last test doesn't run.
As I commented above, neither of the functions above are called, so the expect.assertions(1); code fails the test.
It's possible I need to fundamentally change how I've written the tests, but I'm not sure how I would do that.
Why doesn't that last test pass?
When the code invokes data.pipe(writer), it's running your pipeHandler function defined in the test. This function takes a given writer object and calls writer.emit(...). I believe the issue is that the writer object being passed in is the one mocked out for fs.createWriteStream(), which doesn't have an emit method defined, so nothing is happening in response to that call. It is likely throwing an error, which you may be able to see in your catch block.
I believe what you want is to invoke the handlers saved by the writerOnFn. One way to do so would be to add a property to the object returned by your mock of fs.createWriteStream named emit and define it as a function that invokes the appropriate handler from inside mockWriterEventHandlers. I haven't tested this code but it would look something like the following
const writerEmitFn = (event, arg) => {
mockWriterEventHandlers[event](arg);
}
fs.createWriteStream.mockImplementationOnce(() => ({
on: writerOnFn,
close: writerCloseFn,
emit: writerEmitFn,
}));
My guess is that jest is gobbling up the error.
In order to continue running in the case of exceptions, jest could be guarding against ever having to run try and throw.
You could try expecting an error to have been thrown using jest's API.

How do I unit test the result of a 'then' of a promise in JavaScript?

I'm using TypeScript to write a very simple service that utilizes the AWS SDK. My Jest unit tests are passing, but the coverage reports are saying that the line 'return result.Items' is not covered. Can anyone tell why this is? Is it a bug in jest?
// service file
/**
* Gets an array of documents.
*/
function list(tableName) {
const params = {
TableName: tableName,
};
return docClient
.scan(params)
.promise()
.then((result) => {
return result.Items;
});
}
// test file
const stubAwsRequestWithFakeArrayReturn = () => {
return {
promise: () => {
return { then: () => ({ Items: 'fake-value' }) };
},
};
};
it(`should call docClient.scan() at least once`, () => {
const mockAwsCall = jest.fn().mockImplementation(stubAwsRequest);
aws.docClient.scan = mockAwsCall;
db.list('fake-table');
expect(mockAwsCall).toBeCalledTimes(1);
});
it(`should call docClient.scan() with the proper params`, () => {
const mockAwsCall = jest.fn().mockImplementation(stubAwsRequest);
aws.docClient.scan = mockAwsCall;
db.list('fake-table');
expect(mockAwsCall).toBeCalledWith({
TableName: 'fake-table',
});
});
it('should return result.Items out of result', async () => {
const mockAwsCall = jest
.fn()
.mockImplementation(stubAwsRequestWithFakeArrayReturn);
aws.docClient.get = mockAwsCall;
const returnValue = await db.get('fake-table', 'fake-id');
expect(returnValue).toEqual({ Items: 'fake-value' });
});
The line not covered is the success callback passed to then.
Your mock replaces then with a function that doesn't accept any parameters and just returns an object. The callback from your code is passed to the then mock during the test but it doesn't call the callback so Jest correctly reports that the callback is not covered by your tests.
Instead of trying to return a mock object that looks like a Promise, just return an actual resolved Promise from your mock:
const stubAwsRequestWithFakeArrayReturn = () => ({
promise: () => Promise.resolve({ Items: 'fake-value' })
});
...that way then will still be the actual Promise.prototype.then and your callback will be called as expected.
You should also await the returned Promise to ensure that the callback has been called before the test completes:
it(`should call docClient.scan() at least once`, async () => {
const mockAwsCall = jest.fn().mockImplementation(stubAwsRequest);
aws.docClient.scan = mockAwsCall;
await db.list('fake-table'); // await the Promise
expect(mockAwsCall).toBeCalledTimes(1);
});
it(`should call docClient.scan() with the proper params`, async () => {
const mockAwsCall = jest.fn().mockImplementation(stubAwsRequest);
aws.docClient.scan = mockAwsCall;
await db.list('fake-table'); // await the Promise
expect(mockAwsCall).toBeCalledWith({
TableName: 'fake-table',
});
});
The Library chai-as-promised is worth looking at.
https://www.chaijs.com/plugins/chai-as-promised/
Instead of manually wiring up your expectations to a promise’s
fulfilled and rejected handlers.
doSomethingAsync().then(
function (result) {
result.should.equal("foo");
done();
},
function (err) {
done(err);
}
);
you can write code that expresses what you really mean:
return doSomethingAsync().should.eventually.equal("foo");

Mock imported function with jest in an await context

We are creating a node app, based on express, to read from a static local file and return the JSON within the file.
Here is our json-file.js with our route method:
const readFilePromise = require('fs-readfile-promise');
module.exports = {
readJsonFile: async (req, res) => {
try {
const filePath = 'somefile.json';
const file = await readFilePromise(filePath, 'utf8');
res.send(file);
} catch(e) {
res.status(500).json(e);
}
},
};
we use a third party module, fs-readfile-promise which basically turns node readFileSync into a promise.
But we struggle to mock implementation of this third party, to be able to produce two tests: one based on simulated read file (promise resolved) and one based on rejection.
Here is our test file:
const { readJsonFile } = require('../routes/json-file');
const readFilePromise = require('fs-readfile-promise');
jest.mock('fs-readfile-promise');
const resJson = jest.fn();
const resStatus = jest.fn();
const resSend = jest.fn();
const res = {
send: resSend,
status: resStatus,
json: resJson,
};
resJson.mockImplementation(() => res);
resStatus.mockImplementation(() => res);
resSend.mockImplementation(() => res);
describe('json routes', () => {
beforeEach(() => {
resStatus.mockClear();
resJson.mockClear();
resSend.mockClear();
});
describe('when there is an error reading file', () => {
beforeEach(() => {
readFilePromise.mockImplementation(() => Promise.reject('some error'));
});
it('should return given error', () => {
readJsonFile(null, res);
expect(readFilePromise).lastCalledWith('somefile.json', 'utf8'); // PASS
expect(resStatus).lastCalledWith(500); // FAIL : never called
expect(resSend).lastCalledWith({ "any": "value" }); // FAIL : never called
});
});
});
We tried to place readFilePromise.mockImplementation(() => Promise.reject('some error')); at the top, just after the jest.mock() without more success.
The third party code is basically something like:
module.exports = async function fsReadFilePromise(...args) {
return new Promise(....);
}
How can we mock and replace implementation of the module to return either a Promise.resolve() or Promise.reject() depending on our test setup to make our test case pass within res.send() or res.status() method?
The last 2 assertions never pass because the test doesn't wait for the promise in: const file = await readFilePromise(filePath, 'utf8'); to resolve or reject in this case, therefore res.send or res.status never get called.
To fix it, readJsonFile is async, you should await it in the test:
it('should return given error', async () => {
await readJsonFile(null, res);
...
})
How can we mock and replace implementation of the module to return
either a Promise.resolve() or Promise.reject() depending on our test
setup to make our test case pass within res.send() or res.status()
method
Exactly how you're doing it:
readFilePromise.mockImplementation(() => Promise.reject('some error'));
or
readFilePromise.mockImplementation(() => Promise.resolve('SomeFileContent!'));

Best Way to Test Promises in Jest

Unless I'm misunderstanding something, the resolves and rejects (https://facebook.github.io/jest/docs/expect.html#resolves) won't be available until vNext. What is the recommended way now/in the meantime to test promises with Jest? Is it just putting expects in the thens and catches?
For example:
describe('Fetching', () => {
const filters = {
startDate: '2015-09-01'
};
const api = new TestApiTransport();
it('should reject if no startdate is given', () => {
MyService.fetch().catch(e => expect(e).toBeTruthy()); // see rejects/resolves in v20+
});
it('should return expected data', () => {
MyService.fetch(filters, null, api).then(serviceObjects => {
expect(serviceObjects).toHaveLength(2);
}).catch(e => console.log(e));
});
});
UPDATE 15 June 2019: Not too long after I posted this question, Jest started supporting this out of the box. I changed the accepted answer below to reflect the currently best way to do this.
UPDATE 8 Dec 2021: At some point Jest started supporting async/await. So while other methods noted work, I've taken to simply (for most cases) using something like:
it('should do something', async () => {
const expected = true;
expect(await funcToTest()).toEqual(expected);
});
As with most cases, async/await is much more readable than alternatives. The only case I use resolves or rejects now is for simple cases like:
it('should not throw when doing something', async () => {
await expect(funcToTest()).resolves.not.toThrow();
});
it('should throw when something is wrong', async () => {
await expect(funcToTest()).rejects.toThrow();
});
Nowadays you can write it in this way as well: docs
describe('Fetching', () => {
const filters = {
startDate: '2015-09-01'
};
const api = new TestApiTransport();
it('should reject if no startdate is given', () => {
expect.assertions(1);
return expect(MyService.fetch()).rejects.toEqual({
error: 'Your code message',
});
});
it('should return expected data', () => {
expect.assertions(1);
return expect(MyService.fetch(filters, null, api)).resolves.toEqual(extectedObjectFromApi);
});
});
Update (06.01.2019)
Agree that the accepted answer doesn't work correctly as line
expect.assertions(1); does all the magic. Link to docs
expect.assertions(number) verifies that a certain number of assertions
are called during a test. This is often useful when testing
asynchronous code, in order to make sure that assertions in a callback
actually got called.
So putting this line at the top will control that the specific number of assertions are made by the time when the test is run.
Either return a promise and expect in the resolve or catch
describe('Fetching', () = > {
const filters = {
startDate: '2015-09-01'
};
const api = new TestApiTransport();
it('should reject if no startdate is given', () = > {
return MyService.fetch()
.catch (e => expect(e).toBeTruthy()); // see rejects/resolves in v20+
});
it('should return expected data', () = > {
return MyService.fetch(filters, null, api)
.then(serviceObjects => {
expect(serviceObjects).toHaveLength(2);
})
});
});
or using async/await
describe('Fetching', () = > {
const filters = {
startDate: '2015-09-01'
};
const api = new TestApiTransport();
it('should reject if no startdate is given', async() = > {
try {
const r = await MyService.fetch()
} catch (e) {
expect(e).toBeTruthy()
}
});
it('should return expected data', async() = > {
const serviceObjects = await MyService.fetch(filters, null, api)
expect(serviceObjects).toHaveLength(2);
});
});
I was able to test JEST with AXIOS for HTTP REST calls like this.
it('has an API worth testing', async () => {
let httpResult = null;
await callThefunctionThatReturnsPromiseToMakeTheAxiosApiCall()
.then(function(result) {httpResult=result;})
.catch(function(err) {httpResult=err;});
expect(httpResult.data.myData).toBe("myExpectedValue");
});
or
it('has an API worth testing', async () => {
let httpResult = await callThefunctionThatReturnsPromiseToMakeTheAxiosApiCall();
expect(httpResult.data.myData).toBe("myExpectedValue");
});
For additional Jest matchers maintained by the Jest Community check out jest-extended.
https://jestjs.io/docs/expect
Using jest-extended you can expect your promise toResolve() or toReject(). Then you can expect the result or the error to match something. For example:
test('OK status', async () => {
const request = fetch(...)
await expect(request).toResolve() // First, make sure it resolves
const data = await request
expect(data).toEqual(...) // Then test the result
})
test('ERROR status', async () => {
const request = fetch(...)
await expect(request).toReject() // First, make sure it rejects
await request.catch((error) => expect(error).toBe('...')) // Then test the error
})

how do i test my async jasmine/nodejs/promise code using Spies

I have a module (example has been simplified) called process-promise which has a single function that takes a Promise as input and processes it - it also calls other functions using modules outside it as follows:
//<process-promise.js>
let User = require('user-module');
let processPromise = (promiseObj) => {
let user = new User();
promiseObj.then((full_name) => {
const [ fname, sname ] = full_name.split(' ');
if (fname && sname) {
user.setDetails(fname, sname);
} else{
console.log('nothing happened');
}
}).catch((err) => {
console.log(err.message);
});
};
module.exports = {
processPromise
};
I am trying to unit test the above function using Jasmine, Rewire and Jasmine spies as per following code
let rewire = require('rewire');
let mod = rewire('process-promise');
describe('process-promise module', () => {
beforeEach(() => {
this.fakeUser = createSpyObj('fake-user', ['setDetails']);
this.fakeUserMod = jasmine.createSpy('fake-user-mod');
this.fakeUserMod.and.returnValue(this.fakeUser)
this.revert = mod.__set__({
User: this.fakeUserMod
});
});
afterEach(() => {
this.revert();
});
it('fakeUser.setDetails should be called', (done) => {
mod.processPromise(Promise.resolve('user name'));
done();
expect(this.fakeUser.setDetails).toHaveBeenCalledWith('user','name');
});
});
I expect that the Spy this.fakeUser.setDetails should get called but i get the message from Jasmine "Expected spy fake-user.setAll to have been called with [ 'user', 'name' ] but it was never called." - the problem seems to be the fact the promise is Async but i've included the done function as other SO questions have suggested but this doesn't seem to resolve the problem for me. What's the issue with my code? most other SO questions relate to angular so don't help with my problem.
You are on the right track, the promise is asynchronous and then done function in your test is called before the promise resolved to a value. The done function is used as a callback to tell the test engine, that all your asynchronous code has completed. It should be called after the promise resolved to a value (or failed for that matter).
In order to do that, you'd need to make the following adjustments to your code:
//<process-promise.js>
let User = require('user-module');
let processPromise = (promiseObj) => {
let user = new User();
// return a promise, to allow a client to chain a .then call
return promiseObj.then((full_name) => {
const [ fname, sname ] = full_name.split(' ');
if (fname && sname) {
user.setDetails(fname, sname);
} else{
console.log('nothing happened');
}
}).catch((err) => {
console.log(err.message);
});
};
module.exports = {
processPromise
};
The test would then look like this:
it('fakeUser.setAll should be called', (done) => {
mod.processPromise(Promise.resolve('user name')).then(() => {
expect(this.fakeUser.setAll).toHaveBeenCalledWith('user','name');
done();
}).catch(done);
});
Be sure to add .catch(done). This will make sure your test fails in case the promise resolves to an error.
Is probable that, by the time your test code execute, the promise has not propagated to the code under test. And simply calling done() doesn't the synchronization magic.
I'm not familiar with rewire so I will share an example using
proxyquire
const proxyquire = require('proxyquire');
describe('process-promise module', () => {
const fakeUser = { setDetails: jasmine.createSpy('setDetails') };
const fakeUserMod = jasmine.createSpy('fake-user-mod').and.returnValue(fakeUser);
const promiseObj = Promise.resolve('user name');
beforeEach((done) => {
const processPromiseMod = proxyquire('process-promise', {
'user-module': fakeUserMod,
});
processPromiseMod.processPromise(promiseObj);
promiseObj.then(() => done());
});
it('fakeUser.setDetails should be called', () => {
expect(fakeUser.setDetails).toHaveBeenCalledWith('user','name');
});
});
Also note that setAll doesn't exist in the fakeUser instance. I guess you mean setDetails instead of setAll.

Categories