I have the following simplified example where I'm manually mocking a dependency and getting it to return a resolved Promise. In one of my tests I then attempt to change its return type to a Promise rejection.
If I run all 3 tests together they fail, because the call in the test for rejection actually resolves. However, if I modify line 17 in the spec to it.only('should reject if the client fails to load' then the single test passes.
What am I doing wrong?
service.js:
import client from './client';
const service = (() => {
let instance;
const makeCall = () => {
return true;
};
const init = async () => {
const _client = await client();
return Promise.resolve({
makeCall,
});
};
const getInstance = async () => {
if (!instance) {
instance = await init();
}
return Promise.resolve(instance);
};
return {
getInstance,
};
})();
export default service;
client.js:
const client = () => Promise.resolve('Real');
export default client;
__test__/service.spec.js:
import client from '../client';
import service from '../service';
jest.mock('../client');
describe('Service', () => {
it('should return a singleton', () => {
expect(service.getInstance()).toEqual(service.getInstance());
});
it('should resolve if the client successfully loads', async () => {
expect.assertions(1);
await expect(service.getInstance()).resolves.toHaveProperty('makeCall');
});
it('should reject if the client fails to load', async () => {
const errorMessage = 'Client could not be initialised.';
expect.assertions(1);
client.mockReturnValueOnce(Promise.reject(new Error(errorMessage)));
await expect(service.getInstance()).rejects.toHaveProperty(
'message',
errorMessage
);
});
});
__mocks__/client.js:
const client = jest.fn();
client.mockImplementation(() => {
return Promise.resolve('Mock file');
});
export default client;
This is caused by trying to mock the dependencies of a Singleton.
When running all three tests, the original mock implementation (that resolves) is used and is then not replaced by the second mock implementation that rejects.
It's also worth noting that because the rejected Promise is created but therefore not handled, this results in an unhandled Promise rejection and the test suite crashes.
Related
I'm trying to test method which returns Promise and resolves in another method which I want to mock.
I can test it without mocking but I don't want to because of credentials which wouldn't work on another machine.
Here's my Service code (PollyService.ts):
#Service()
export class PollyService {
#Inject()
private polly: Polly; // aws-sdk Polly
url: string;
getSpeech(body: any) {
return new Promise((resolve) => {
let voice: string = (voices as any)[body.code];
let params = {
exampleParam: example
};
this.polly.synthesizeSpeech(params, (error: any, data: any) => {
if (error) throw error;
else {
resolve(data);
}
});
});
}
}
Here's test (app.spec.ts)
describe('PollyService', () => {
afterEach(() => {
Container.reset();
});
it ('Should return data', async () => {
const event: any = {
body: {
sentence: "potato",
code: "en"
}
};
let polly = Container.get(Polly); // aws-sdk polly
spyOn(polly, 'synthesizeSpeech'); // and here I want to resolve Promise
await Container.get(PollyService).getSpeech(event.body);
});
});
When I am mocking that without resolving promise there is error:
Error: Timeout - Async function did not complete within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
How can i simulate resolving Promise in tests?
If you want to create a promise which is already resolved, you can simply use Promise.resolve(someData)
In your case it should be something like this:
spyOn(polly, 'synthesizeSpeech').and.returnValue(Promise.resolve('mockedData'));
Note: You get the timeout error message because you are neither returning the promise in the end of your function nor make use of the done callback.
either do:
...
return await Container.get(PollyService).getSpeech(event.body);
or
describe('PollyService', () => {
afterEach(() => {
Container.reset();
});
it ('Should return data', async (done) => { // <- done is the callback
const event: any = {
body: {
sentence: "potato",
code: "en"
}
};
let polly = Container.get(Polly);
spyOn(polly, 'synthesizeSpeech');
await Container.get(PollyService).getSpeech(event.body);
done(); // <- calling it will tell jasmine your test is over
});
});
Usually either you choose to return the promise or use the callback. I am not sure on how jasmine mix async methods and the callback in the same method but it should work. More information here
You should extend the spyOn with and.returnValue as follows:
const data = ... // define the data to be returned by synthesizeSpeech
spyOn(polly, 'synthesizeSpeech').and.returnValue(Promise.resolve(data));
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.
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");
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!'));
I have a js file which supplies some db operations. This file works with promises only which can be chained. To test that class I work with an async function.
The problem is, that whenever I work with promises inside my test function the it function gets blocked for every other test later.
Here are two examples:
'use strict'
const exec = require('child_process').exec
const path = require('path')
const request = require('request')
const expect = require('chai').expect
const createTableStatements = require('../data')
test()
async function test () {
await testGetUser()
console.log('1')
await testGetFaculties()
}
function testGetUser () {
return new Promise((resolve1) => {
describe('test get user', function () {
const db = require('../dbInterface')
it('test get user should be complete', function () {
db.dbFunctions.dropAll()
.then(onResolve => {
return db.dbFunctions.createTable(createTableStatements.createTableStatements.user)
}
)
.then(() => {
console.log('success create user table')
return db.dbFunctions.addUser('1', 'firstName', 'lastName', 'email')
})
.then(resolve => {
return db.dbFunctions.getUser('email', undefined)
})
.then(result => {
expect(result.toString().includes('dummy')).to.equal(false)
})
.then(resolve => {
return db.dbFunctions.dropAll()
})
.then(resolve => {
console.log('resolve')
resolve1()
})
.catch(err => console.log(err))
})
})
})
}
function testGetFaculties () {
return new Promise(resolve => {
describe('test get faculties', function () {
let db
before(function () {
db = require('../dbInterface')
})
console.log('displayed')
it('should work', function () {
console.log('locked')
expect(db.dbFunctions.getFaculties('hsa')).to.be.an('array').that.does.include('Science')
resolve()
})
})
})
}
And this is the output
resolve
1
displayed
As you can see console.log('locked') is not being processed.
What i figured out so far, that I only have this issue when I call expect within a then function. But this is necessary for my tests.
The test () function should contain much more tests, only for this question I shortened it.
And for clarification: If I only test methods type of testGetFaculties () which don't contains another promise chain inside it works like it should.
Any idea why this is like it is?
Most probably the console.log( 'locked' ); doesn't do anything, because your previous test case was not finished at all.
Writing describe, it, before inside a Promise and containing unreturned Promises is something that you should not do.
Much better test case would look like :
'use strict'
const exec = require('child_process').exec
const path = require('path')
const request = require('request')
const expect = require('chai').expect
const createTableStatements = require('../data')
// You use this in both test cases anyway
const db = require('../dbInterface');
describe('test get user', function () {
it('test get user should be complete', function () {
return db
// ^ returning promise will make sure that the test ends when the promise ends.
.dbFunctions
.dropAll()
.then(onResolve => { ... } )
...
)
} );
} );
describe('test get faculties', function () {
it('should work', function () {
return db
// ^ returning promise will make sure that the test ends when the promise ends.
.dbFunctions
.getFaculties('hsa')
.then( value => {
// ^ You actually need to test the value of the resolve promise
expect( value ).to.be.an('array').that.does.include('Science');
} )
} );
} );