I've tried to create a class to connect to a mongoDB (and get a gridFS connection using (gridfs-stream
). But with that I do get two problems:
server instance in invalid state connected
So I would be very thankful if someone can help me to optimize this class to get a really solid working class. For example I don't like the let that = this
in the connect() function.
Example repo
DB class
const mongo = require('mongodb')
const Grid = require('gridfs-stream')
const { promisify } = require('util')
export default class Db {
constructor (uri, callback) {
this.db = null
this.gfs = null
const server = process.env.MONGO_SERVER || 'localhost'
const port = process.env.MONGO_PORT || 27017
const db = process.env.MONGO_DB || 'test'
// Is this the correct way to connect (using mongo native driver)?
this.connection = new mongo.Db(db, new mongo.Server(server, port))
this.connection.open = promisify(this.connection.open)
this.connected = false
return this
}
async connect (msg) {
let that = this
if (!this.db) {
try {
await that.connection.open()
that.gfs = Grid(that.connection, mongo)
this.connected = true
} catch (err) {
console.error('mongo connection error', err)
}
}
return this
}
isConnected () {
return this.connected
}
}
Example
This function will add a new user to the DB using the class above:
import bcrypt from 'bcrypt'
import Db from './lib/db'
const db = new Db()
export async function createUser (obj, { username, password }) {
if (!db.isConnected()) await db.connect()
const Users = db.connection.collection('users')
return Users.insert({
username,
password: bcrypt.hashSync(password, 10),
createdAt: new Date()
})
}
Unit test
I need to create a unit test to test if the mongoDB method is called. No integration test for testing the method. So I need to mock the DB connection, collection and insert method.
import bcrypt from 'bcrypt'
import { createUser } from '../../user'
import Db from '../../lib/db'
const db = new Db()
jest.mock('bcrypt')
describe('createUser()', () => {
test('should call mongoDB insert()', async () => {
bcrypt.hashSync = jest.fn(() => SAMPLE.BCRYPT)
// create somekind of mock for the insert method...
db.usersInsert = jest.fn(() => Promise.resolve({ _id: '507f1f77bcf86cd799439011' }))
await createUser({}, {
username: 'username',
password: 'password'
}).then((res) => {
// test if mocked insert method has been called
expect(db.usersInsert).toHaveBeenCalled()
// ... or better test for the returned promise value
})
})
})
There are multiple ways to go about this. I will list few of them
I will showcase the first case here, which you posted about with code and how to make it work. So first thing we would do is update the __mocks__/db.js
file to below
jest.mock('mongodb');
const mongo = require('mongodb')
var mock_collections = {};
var connectError = false;
var connected = false;
export default class Db {
constructor(uri, callback) {
this.__connectError = (fail) => {
connected = false;
connectError = fail;
};
this.clearMocks = () => {
mock_collections = {};
connected = false;
};
this.connect = () => {
return new Promise((resolve, reject) => {
process.nextTick(
() => {
if (connectError)
reject(new Error("Failed to connect"));
else {
resolve(true);
this.connected = true;
}
}
);
});
};
this.isConnected = () => connected;
this.connection = {
collection: (name) => {
mock_collections[name] = mock_collections[name] || {
__collection: name,
insert: jest.fn().mockImplementation((data) => {
const ObjectID = require.requireActual('mongodb').ObjectID;
let new_data = Object.assign({}, {
_id: new ObjectID()
},data);
return new Promise((resolve, reject) => {
process.nextTick(
() =>
resolve(new_data))
}
);
})
,
update: jest.fn(),
insertOne: jest.fn(),
updateOne: jest.fn(),
};
return mock_collections[name];
}
}
}
}
Now few explanations
jest.mock('mongodb');
will make sure any actual mongodb call gets mockedconnected
, connectError
, mock_collections
are global variables. This is so that we can impact the state of the Db
that your user.js
loads. If we don't do this, we won't be able to control the mocked Db
from within our teststhis.connect
shows how you can return a promise and also how you can simulate a error connecting to DB when you wantcollection: (name) => {
makes sure that your call to createUser
and your test can get the same collection interface and check if the mocked functions were actually called.insert: jest.fn().mockImplementation((data) => {
shows how you can return data by creating your own implementationconst ObjectID = require.requireActual('mongodb').ObjectID;
shows how you can get an actual module object when you have already mocked mongodb
earlierNow comes the testing part. This is the updated user.test.js
jest.mock('../../lib/db');
import Db from '../../lib/db'
import { createUser } from '../../user'
const db = new Db()
describe('createUser()', () => {
beforeEach(()=> {db.clearMocks();})
test('should call mongoDB insert() and update() methods 2', async () => {
let User = db.connection.collection('users');
let user = await createUser({}, {
username: 'username',
password: 'password'
});
console.log(user);
expect(User.insert).toHaveBeenCalled()
})
test('Connection failure', async () => {
db.__connectError(true);
let ex = null;
try {
await createUser({}, {
username: 'username',
password: 'password'
})
} catch (err) {
ex= err;
}
expect(ex).not.toBeNull();
expect(ex.message).toBe("Failed to connect");
})
})
Few pointers again
jest.mock('../../lib/db');
will make sure that our manual mock gets loadedlet user = await createUser({}, {
since you are using async
, you will not use then
or catch
. That is the point of using async
function. db.__connectError(true);
will set the global variable connected
to false
and connectError
to true. So when createUser
gets called in the test it will simulate a connection errorex= err;
, see how I capture the exception and take out the expect call. If you do expect in the catch
block itself, then when an exception is not raised the test will still pass. That is why I have done exception testing outside the try/catch
blockNow comes the part of testing it by running npm test
and we get
All of it is committed to below repo you shared
https://github.com/jaqua/mongodb-class
You are stubbing on an instance of DB, not the actual DB class. Additionally I don't see the db.usersInsert
method in your code. We can't write your code for you, but I can point you in the right direction. Also, I don 't use Jest but the concepts from Sinon are the same. The best thing to do in your case I believe is to stub out the prototype of the class method that returns an object you are interacting with.
Something like this:
// db.js
export default class Db {
getConnection() {}
}
// someOtherFile.js
import Db from './db';
const db = new Db();
export default async () => {
const conn = await db.getConnection();
await connection.collection.insert();
}
// spec file
import {
expect
} from 'chai';
import {
set
} from 'lodash';
import sinon from 'sinon';
import Db from './db';
import testFn from './someOtherFile';
describe('someOtherFile', () => {
it('should call expected funcs from db class', async () => {
const spy = sinon.spy();
const stub = sinon.stub(Db.prototype, 'getConnection').callsFake(() => {
return set({}, 'collection.insert', spy);
});
await testFn();
sinon.assert.called(spy);
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With