Reputation: 17332
In my application code there are several places, where I have to connect to a DB and get some data. For my unit tests (I'm using JestJS), I need to mock this out.
Let's assume this simple async function:
/getData.js
import DB from './lib/db'
export async function getData () {
const db = DB.getDB()
const Content = db.get('content')
const doc = await Content.findOne({ _id: id })
return doc
}
The DB connection is in a separate file:
/lib/db.js
import monk from 'monk'
var state = {
db: null
}
exports.connect = (options, done) => {
if (state.db) return done()
state.db = monk(
'mongodb://localhost:27017/db',
options
)
return state.db
}
exports.getDB = () => {
return state.db
}
You can see, I'll recieve the DB and get a collection. After this I will recieve the data.
My attempt for the mock so far:
/tests/getData.test.js
import { getData } from '../getData'
import DB from './lib/db'
describe('getData()', () => {
beforeEach(() => {
DB.getDB = jest.fn()
.mockImplementation(
() => ({
get: jest.fn(
() => ({
findOne: jest.fn(() => null)
})
)
})
)
})
test('should return null', () => {
const result = getData()
expect(result).toBeNull()
})
})
Maybe this is not the best way to do it...? I'm very happy for every improvement.
My question is where to put the DB mock as there are multiple tests and every test needs a different mock result for the findOne()
call.
Maybe it is possible to create a function, which gets called with the needed parameter or something like that.
Upvotes: 1
Views: 5606
Reputation: 8662
First I just want to note that testing this proof-of-concept function as-is appears low in value. There isn't really any of your code in there; it's all calls to the DB client. The test is basically verifying that, if you mock the DB client to return null, it returns null. So you're really just testing your mock.
However, it would be useful if your function transformed the data somehow before returning it. (Although in that case I would put the transform in its own function with its own tests, leaving us back where we started.)
So I'll suggest a solution that does what you asked, and then one that will hopefully improve your code.
getData()
- Not Recommended:You can create a function that returns a mock that provides a findOne()
that returns whatever you specify:
// ./db-test-utils
function makeMockGetDbWithFindOneThatReturns(returnValue) {
const findOne = jest.fn(() => Promise.resolve(returnValue));
return jest.fn(() => ({
get: () => ({ findOne })
}));
}
Then in your code file, call DB.getDB.mockImplementation
in beforeEach or beforeAll above each test, passing in the desired return value, like this:
import DB from './db';
jest.mock('./db');
describe('testing getThingById()', () => {
beforeAll(() => {
DB.getDB.mockImplementation(makeMockGetDbWithFindOneThatReturns(null));
});
test('should return null', async () => {
const result = await getData();
expect(result).toBeNull();
});
});
This question is really exciting, because it is a wonderful illustration of the value of having each function do only one thing!
getData
appears to be very small - only 3 lines plus a return
statement. So at first glance it doesn't seem to be doing too much.
However, this tiny function has very tight coupling with the internal structure of DB
. It has dependency on:
DB
- a singletonDB.getDB()
DB.getDB().get()
DB.getDB().get().findOne()
This has some negative repercussions:
DB
ever changes its structure, which since it uses a 3rd party component, is possible, then every function you have that has these dependencies will break.getDB()
and db.get('collection')
, resulting in repeated code.Here's one way you could improve things, while making your test mocks much simpler.
db
instead of DB
I could be wrong, but my guess is, every time you use DB
, the first thing you'll do is call getDB()
. But you only ever need to make that call once in your entire codebase. Instead of repeating that code everywhere, you can export db
from ./lib/db.js
instead of DB
:
// ./lib/db.js
const DB = existingCode(); // However you're creating DB now
const dbInstance = DB.getDB();
export default dbInstance;
Alternatively, you could create the db instance in a startup function and then pass it in to a DataAccessLayer class, which would house all of your DB access calls. Again only calling getDB()
once. That way you avoid the singleton, which makes testing easier because it allows dependency injection.
// ./lib/db.js
const DB = existingCode(); // However you're creating DB now
const dbInstance = DB.getDB();
export function getCollectionByName(collectionName){
return dbInstance.get(collectionName);
}
export default dbInstance;
This function is so trivial it might seem unnecessary. After all, it has the same number of lines as the code it replaces! But it removes the dependency on the structure of dbInstance
(previously db
) from calling code, while documenting what get()
does (which is not obvious from its name).
Now your getData
, which I'm renaming getDocById
to reflect what it actually does, can look like this:
import { getCollectionByName } from './lib/db';
export async function getDocById(id) {
const collection = getCollectionByName('things');
const doc = await collection.findOne({ _id: id })
return doc;
}
Now you can mock getCollectionByName separately from DB:
// getData.test.js
import { getDocById } from '../getData'
import { getCollectionByName } from './lib/db'
jest.mock('./lib/db');
describe('testing getThingById()', () => {
beforeEach(() => {
getCollectionByName.mockImplementation(() => ({
findOne: jest.fn(() => Promise.resolve(null))
}));
});
test('should return null', async () => {
const result = await getDocById();
expect(result).toBeNull();
});
});
This is just one approach and it could be taken much further. For example we could export findOneDocById(collectionName, id)
and/or findOneDoc(collectionName, searchObject)
to make both our mock and calls to findOne()
simpler.
Upvotes: 7