Jest: How to properly test a Javascript service using MongoDB - javascript

I'm a total beginner with Jest.
I've got a UserService using Dependency Injection.
public async getAll() {
const userRecords = await this.userModel.find().select('name').catch((e) => {
throw new HttpException(500, 'Error while fetching users.', e)
});
return <[IUser]>userRecords;
}
I would like to test this feature. Here are the tests I could run:
Calling the route, and checking if the resulting JSON is OK
Ggetting DB content, and checking if it is as expected
Just test the getAll function
I think 1 and 2 are obvious, and cover different kind of things. 1 covers the request part, 2 covers the DB part. But what about number 3? How to "just test" the getAll function?
I've tried this:
const userModel = {
find: (user) => {
return [
{ id: 'user1' },
{ id: 'user2' }
]
},
};
const userService = new UserService(userModel);
const userRecords = await userService.getAll();
expect(argumentRecord).toBeDefined();
But obviously it's failing because select is undefined.
Should I also mock select()? Should I organize my code differently?

If were to write this test I would mock the functions using jest.fn(implementation) so that expectations can be enforced on the function calls.
const userQuery = {
select: jest.fn(() => Promise.resolve([]))
};
const userModel = {
find: jest.fn(() => userQuery)
};
const userService = new UserService(userModel);
const userRecords = await userService.getAll();
expect(userRecords).toEqual([]);
expect(userModel.find).toHaveBeenCalled();
expect(userQuery.select).toHaveBeenCalledWith('name');
Performing expectations on the function calls may sound like overkill, but it explicitly verifies that the mock is actually being used by getAll.
I would also structure the tests in such a way that I can test the various code paths without re-implementing the entire mock.
describe('getAll()', () => {
let userQuery, userModel, userService;
beforeEach(() => {
userQuery = {
select: jest.fn(() => Promise.resolve([]))
};
userModel = {
find: jest.fn(() => userQuery)
};
userService = new UserService(userModel);
});
afterEach(() => {
expect(userModel.find).toHaveBeenCalled();
expect(userQuery.select).toHaveBeenCalledWith('name');
});
it('should get the user names', async () => {
const users = [{
name: 'john'
}, {
name: 'jane'
}];
userQuery.select.mockImplementation(() => Promise.resolve(users));
await expect(userService.getAll()).resolves.toBe(users);
});
it('should handle errors', async () => {
const error = new Error('Fake model error');
userQuery.select.mockImplementation(() => Promise.reject(error));
await expect(userService.getAll()).rejects.toMatch({
status: 500,
message: 'Error while fetching users.',
cause: error
});
});
});
This code is untested, so it may not work correctly, but hopefully it outlines the idea sufficiently.
While this is not directly related to your question I would avoid mixing async/await with traditional promise handling.
public async getAll() {
try {
return <[IUser]> await this.userModel.find().select('name');
} catch (e) {
throw new HttpException(500, 'Error while fetching users.', e)
}
}

Yes, you should mock select. And not only that, but everything that is used inside the function and test if they are executed properly. I would do this:
class SomeClass {
public async getAll() {
const userRecords = await this.userModel.find().select('name').catch(this.errorHandler);
return <[IUser]>userRecords;
}
public errorHandler(e) {
throw new HttpException(500, 'Error while fetching users.', e);
}
}
// this is just an example, it should be the same type as your expected returned output
const whatever = Math.random();
const fakeCatch = jest.fn(() => whatever);
const fakeSelect = jest.fn(() => {
return {
catch: fakeCatch
}
});
const fakeFind = jest.fn(() => {
return {
select: fakeSelect
};
});
const fakeUserModel = {
find: fakeFind,
}
const userService = new UserService(fakeUserModel);
const userRecords = await userService.getAll();
// should return the correct result
expect(userRecords).toEqual(whatever);
// should execute find
expect(fakeFind).toHaveBeenCalledTimes(1);
// should execute select with 'name' parameter
expect(fakeSelect).toHaveBeenCalledTimes(1);
expect(fakeSelect).toHaveBeenCalledWith('name');
// should execute catch with this.errorHandler
expect(fakeCatch).toHaveBeenCalledWith(userService.errorHandler);

Related

Jest does not detect my mock call in promise .catch block

Jest does not seem to be detecting my mock calls in the catch block. Yet, if don't mock the method handleError, I'll receive an error. I have tried multiple ways of doing the returned rejected promise but no luck so far.
I've tried callbacks, Promise.reject, Promise((res, rej) => {})
Code being tested:
module.exports = class PresetDropdown {
constructor (Api, objectId, titleSort) {
this.Api = Api;
this.objectId; = objectId;
this.presets = [];
}
handleError (err) {
console.log(err)
// more functionality
}
get () {
this.Api.getMany(this.objectId)
.then((data) => {
this.handleEmpty(data);
this.isError = false;
})
.catch((err) => {
this.isError = true;
this.handleError(err);
});
}
};
Test (Jest):
test('Expect call from handleError after reject promise', async () => {
// assemble
// return the class
const Module = getModule();
const mockRejectedPromise = jest.fn(() => {
return Promise.reject(Error(mockError));
});
mockApi.getMany = mockRejectedPromise;
const module = new Module(mockApi, '1', null);
const mockHandleError = jest.fn(() => {});
PresetTemplate.handleError = mockHandleError;
// act
await PresetTemplate.get(mockScope);
// assert
expect(mockHandleError).toHaveBeenCalledTimes(1); // DOES NOT DETECT CALL
// Test CASE FAILS HERE ^
});
Since PresetDropdown class accept an Api object, you can create a mocked Api object and pass it to the class. Then mock resolved/rejected value for Api.getMany() method, so that you can test different code branches.
E.g.
presetDropdown.js:
module.exports = class PresetDropdown {
constructor(Api, objectId, titleSort) {
this.Api = Api;
this.objectId = objectId;
this.presets = [];
}
handleError(err) {
console.log(err);
}
handleEmpty(data) {
console.log(data);
}
get() {
return this.Api.getMany(this.objectId)
.then((data) => {
this.handleEmpty(data);
this.isError = false;
})
.catch((err) => {
this.isError = true;
this.handleError(err);
});
}
};
presetDropdown.test.js:
const PresetDropdown = require('./presetDropdown');
describe('71164955', () => {
test('should handle data', async () => {
const mockApi = {
getMany: jest.fn().mockResolvedValueOnce('fake data'),
};
const instance = new PresetDropdown(mockApi, '1');
await instance.get();
expect(instance.isError).toBeFalsy();
});
test('should handle error', async () => {
const mockError = new Error('fake error');
const mockApi = {
getMany: jest.fn().mockRejectedValueOnce(mockError),
};
const instance = new PresetDropdown(mockApi, '1');
await instance.get();
expect(instance.isError).toBeTruthy();
});
});
Test result:
PASS stackoverflow/71164955/presetDropdown.test.js
71164955
✓ should handle data (14 ms)
✓ should handle error (3 ms)
console.log
fake data
at PresetDropdown.handleEmpty (stackoverflow/71164955/presetDropdown.js:11:13)
console.log
Error: fake error
at Object.<anonymous> (/Users/dulin/workspace/github.com/mrdulin/jest-v26-codelab/stackoverflow/71164955/presetDropdown.test.js:12:23)
at Object.asyncJestTest (/Users/dulin/workspace/github.com/mrdulin/jest-v26-codelab/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:106:37)
at /Users/dulin/workspace/github.com/mrdulin/jest-v26-codelab/node_modules/jest-jasmine2/build/queueRunner.js:45:12
at new Promise (<anonymous>)
at mapper (/Users/dulin/workspace/github.com/mrdulin/jest-v26-codelab/node_modules/jest-jasmine2/build/queueRunner.js:28:19)
at /Users/dulin/workspace/github.com/mrdulin/jest-v26-codelab/node_modules/jest-jasmine2/build/queueRunner.js:75:41
at processTicksAndRejections (internal/process/task_queues.js:93:5)
at PresetDropdown.handleError (stackoverflow/71164955/presetDropdown.js:8:13)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.311 s

Why is this not working? Testing non exported-function with Rewire / Jest

Pretty much running the exact same code elsewhere however, this time, I am using rewire to test a non-exported function at helper.js.
When debugging the following I get to return await db.collection('foo') which returns undefined and throwing when attempting 'undefined.findOne()' naturally. My understanding is that db.collection('foo') should return mockDbConnect in this case allowing to chain findOne, which it is not doing...
Help is appreciated.
foo.test.js
const rewire = require('rewire');
const helper = rewire('../../../src/models/helper/helpers');
describe('model helpers', () => {
describe('searchValue', () => {
it('should return an instance', async () => {
const searchValue = helper.__get__('searchValue')
const mockFoo = { foo: '1' }
const fooId = '1';
const mockDbConnect = {
collection: jest.fn().mockReturnThis(),
findOne: jest.fn().mockReturnValue(mockFoo)
}
try {
const res = await searchValue(fooId, mockDbConnect);
expect(mockDbConnect.collection).toHaveBeenCalledWith('foos');
} catch (err) {
throw err;
}
});
})
})
helpers.js
const searchValue = async (fooId, db) => {
return await db
.collection('foos')
.findOne({ fooId: fooId });
};

How to mock sns?

I faced to some issue when I wanted to test my handler which publishes a message on SNS.
Here is my code:
// my handler
export const handler = (event) => {
try {
await emitDeletionComplete(classified.metadata.classifiedRequestId);
}
catch(e) {
console.error(e);
throw new Error(e);
}
}
// my SNS service
import SNS from 'aws-sdk/clients/sns';
const snsClient = new SNS({region: process.env.AWS_REGION});
export const emitDeletionComplete = async (id) => {
try {
await snsClient.publish({
Message: JSON.stringify({
type: 'DELETE_COMPLETE',
data: {
id
}
}),
TopicArn: process.env.SNS_ARN
}).promise();
} catch(err) {
console.error(err, err.stack);
throw new Error('We do not succeed to publish the message DELETE_COMPLETE to ARN: ' + process.env.SNS_ARN);
}
};
When i want to test, i try to do :
import { handler } from '../../../src/handler/dispatch-deletion-to-legacy';
import SNS from 'aws-sdk/clients/sns';
jest.mock('aws-sdk/clients/sns', () => {
return {
__esModule: true,
default: jest.fn(() => {
return {
publish: jest.fn().mockReturnThis(),
promise: jest.fn(),
}
}),
};
});
[...]
it('should delete', () => {
let sns = new SNS();
const event = {
Records: [
{
body: JSON.stringify({...some event...})
}
]
}
handler(event);
expect(sns.publish().promise).toBeCalledTimes(1);
});
Apparently, it is never called. I don't get why.Maybe my mock is completely wrong.
i'm stuck with it for few hours now...Any idea how can I mock correctly ?
EDIT 1 : https://github.com/JLGouwy/aws-sns-mock-test
thanks
In short, your instance - let sns = new SNS(); is not instance used by your production code.
In long, you have keeping track of usage of sns mock instance, jest document
This is your example, but I changed a little. I will try to explain by comments in bellow example.
Production code:
import SNS from 'aws-sdk/clients/sns';
const snsClient = new SNS({ region: process.env.AWS_REGION });
exports.handler = async (event) => {
try {
const { classifiedRequestId } = JSON.parse(event.Records[0].body); // try to get classifiedRequestId from Record body
await emitDeletionComplete(classifiedRequestId);
} catch (e) {
console.error(e);
throw new Error(e);
}
}
const emitDeletionComplete = async (id) => { // in the same file with handler function
try {
await snsClient.publish({
Message: JSON.stringify({
type: 'DELETE_COMPLETE',
data: {
id
}
}),
TopicArn: process.env.SNS_ARN
}).promise();
} catch (err) {
console.error(err, err.stack);
throw new Error('We do not succeed to publish the message DELETE_COMPLETE to ARN: ' + process.env.SNS_ARN);
}
};
Spec file
import SNS from 'aws-sdk/clients/sns';
import { handler } from "."; // handler function in the same directory
const mockPromise = jest.fn(); // mock for deep function - promise
jest.mock('aws-sdk/clients/sns', () => {
// return a function as a constructor
return jest.fn().mockImplementation(function () { // "normal function" not arrow function
this.publish = jest.fn(() => ({ // mock publish function
promise: mockPromise, // returns an object what includes promise property
}));
});
});
describe("handler function", () => {
it("should publish delete command", async () => {
const classifiedRequestId = "12345";
const event = {
Records: [
{
body: JSON.stringify({ // input data
classifiedRequestId,
}),
}
]
};
await handler(event);
// get instance of SNS, an instance has been created in production code
const snsMocked = SNS.mock.instances[0];
// expect publish function will be call with expected parameter
expect(snsMocked.publish).toHaveBeenCalledWith(
{
Message: JSON.stringify({
type: 'DELETE_COMPLETE',
data: {
id: classifiedRequestId,
}
}),
TopicArn: process.env.SNS_ARN
}
);
// expect promise function will be call too
expect(mockPromise).toHaveBeenCalled();
})
});

Jest mock function not working as expected

I'm trying to create a mock for a function createClient where it should return a specific object.
However, for some reason the mock is ignored and it runs the function instead of receiving the mock value.
authorization.js
// some requires here
const createClient = req => {
if (!(req.user && req.user.access_token)) {
throw new Error('Not authorized');
}
...
return { ... }
}
const getUser = async client => { ... }
module.exports = options => {
...
createClient(req) is called here
...
}
authorization.test.js
import authorization from '../../server/middlewares/authorization';
describe('authorization.js', () => {
it('Should do something', async done => {
authorization.createClient = jest.fn(() => ({
client: 'test',
}));
// ACT
const authorizationMiddleware = authorization();
const result = await authorizationMiddleware(someOptions);
// ASSERT
expect(result).toBe('authorized');
done();
});
The error
It seems that the mock for createClient is not working as I wanted to be. It should return the object { client: 'test' }
Your code is incomplete, so I try to give a demo for your case. If you want to mock a private variable in the module scope, the createClient function for your case. You can use rewire package to do that.
E.g.
authorization.js
let createClient = (req) => {
if (!(req.user && req.user.access_token)) {
throw new Error('Not authorized');
}
function getUser() {
return 'real user';
}
return { getUser };
};
const getUser = async (client) => {
return client.getUser();
};
module.exports = (options) => {
const client = createClient(options.req);
return () => getUser(client);
};
authorization.test.js:
const rewire = require('rewire');
describe('61076881', () => {
it('should get user', async () => {
const authorization = rewire('./authorization');
const mClient = { getUser: jest.fn().mockReturnValueOnce('fake user') };
const mCreateClient = jest.fn(() => mClient);
authorization.__set__('createClient', mCreateClient);
const options = { req: { user: { access_token: '123' } } };
const authorizationMiddleware = authorization(options);
const user = await authorizationMiddleware();
expect(user).toEqual('fake user');
expect(mCreateClient).toBeCalledWith(options.req);
expect(mClient.getUser).toBeCalledTimes(1);
});
});
unit test results:
PASS stackoverflow/61076881/authorization.test.js (7.601s)
61076881
✓ should get user (10ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.54s, estimated 9s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/61076881

Injecting a stubbed dependency into an es6 class with Sinon

I have the following modules:
const rp = require('request-promise');
// dummies.repository.js
exports.getDummiesData = () => {
const options = {
url: 'https://api.github.com/users',
headers: {
'User-Agent': 'Request-Promise',
},
json: true
};
return rp(options)
.then(r => r)
.catch(err => { throw err; });
}
The above file is a dependency to a class that I would like to test. I inject it to the below class via its constructor like so:
// dummies.service.js
const Dummies = require('./dummies.model');
class DummiesService {
constructor(dummiesRepository) {
this.dummiesRepository = dummiesRepository;
}
getDummies() {
return new Promise((resolve, reject) => {
this.dummiesRepository.getDummiesData()
.then((response) => {
// business logic
const dummies = response.map(d => new Dummies(d));
resolve(dummies);
})
.catch((response) => {
reject(response);
});
});
}
}
module.exports = DummiesService;
In the above, I want to test the method getDummies with sinon. My approach so far has been to stub out the dummiesRepository and inject that to my dummies service during instantiation, but I am getting an error. Here is how my test looks:
const DummiesService = require('../dummies.service');
const dummiesRepository = require('../dummies.repository');
let dummiesRepositoryStub;
afterEach(() => {
if(dummiesRepositoryStub){
dummiesRepositoryStub.restore();
}
});
describe('unit: dummies.service - when getting dummies data', () => {
it('it should resolve and return dummies data', () => {
const dummiesResponse = [
{ id: 1, login: 'dummy1', additionalData: 'data' },
{ id: 2, login: 'dummy2', additionalData: 'data' },
{ id: 3, login: 'dummy3', additionalData: 'data' },
];
dummiesRepositoryStub = sinon
.stub(dummiesRepository, 'getDummiesData')
.resolves(dummiesResponse);
dummiesService = new DummiesService(dummiesRepositoryStub);
// act + assert
return dummiesService.getDummies()
.then((response) => {
chai.expect(dummiesRepositoryStub.called).to.equal(true);
chai.expect(response).to.deep.equal(dummiesTransformedModel);
});
});
This test fails with error TypeError: this.dummiesRepository.getDummiesData is not a function. I am not sure why this is not working, I injected a stub implementation of dummiesRepository to the DummiesService class. Please assist.
I could not find a way to do constructor injection in the above code, but I found a work around to allow sinon to stub my method.
I had to remove dummyRepository from being a constructor dependency to just using require to include it. If the code looks as follows
const Dummies = require('./dummies.model');
const dummiesRepository = require('./dummies.repository');
class DummiesService {
constructor() {
this.dummiesRepository = dummiesRepository;
}
getDummies() {
return new Promise((resolve, reject) => {
this.dummiesRepository.getDummiesData()
.then((response) => {
// business logic
const dummies = response.map(d => new Dummies(d));
resolve(dummies);
})
.catch((response) => {
reject(response);
});
});
}
}
module.exports = DummiesService;
and the test is written the same as above, but instantiating DummyService without the dummyRepositoryStub as a parameter, everything works fine.
This works, but I would still like to know if there is a way to inject the dependency via the constructor, because this approach requires me to refactor a lot of my code.

Categories

Resources