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:
我试图创建一个连接到mongoDB的类(并使用(gridfs-stream)获取gridFS连接。但是我确实遇到了两个问题:
- I do get sometimes the mongo Error
server instance in invalid state connected
- It is impossible for me to mock this class out - using jestJS
我确实有时连接无效状态的mongo错误服务器实例
我不可能使用jestJS来模拟这个课程
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.
所以如果有人能帮助我优化这个课程以获得一个非常稳固的工人阶级,我将非常感激。例如,我不喜欢在connect()函数中使用let = this。
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:
此函数将使用上面的类向DB添加新用户:
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.
我需要创建一个单元测试来测试是否调用了mongoDB方法。没有用于测试该方法的集成测试。所以我需要模拟数据库连接,集合和插入方法。
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
})
})
})
2 个解决方案
#1
2
There are multiple ways to go about this. I will list few of them
有很多方法可以解决这个问题。我会列出其中的一些
- Mock the DB class using a Jest manual mock. This could be cumbersome if you are using too many mongo functions. But since you are encapsulating most through the DB class it may still be manageable
- Use a mocked mongo instance. This project allows you to simulate a MongoDB and persist data using js file
- Use a in-memory mongodb
- Use a actual mongodb
使用Jest手动模拟来模拟DB类。如果你使用太多的mongo函数,这可能很麻烦。但是,由于您通过数据库类进行封装,因此它仍然可以管理
使用模拟的mongo实例。此项目允许您使用js文件模拟MongoDB并保留数据
使用内存中的mongodb
使用实际的mongodb
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
我将在这里展示第一个案例,您发布了代码以及如何使其工作。所以我们要做的第一件事是将__mocks __ / db.js文件更新到下面
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 mocked - The
connected
,connectError
,mock_collections
are global variables. This is so that we can impact the state of theDb
that youruser.js
loads. If we don't do this, we won't be able to control the mockedDb
from within our tests -
this.connect
shows how you can return a promise and also how you can simulate a error connecting to DB when you want -
collection: (name) => {
makes sure that your call tocreateUser
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 implementation -
const ObjectID = require.requireActual('mongodb').ObjectID;
shows how you can get an actual module object when you have already mockedmongodb
earlier
jest.mock( 'mongodb的');将确保任何实际的mongodb调用被嘲笑
connected,connectError,mock_collections是全局变量。这样我们就可以影响你的user.js加载的Db的状态。如果我们不这样做,我们将无法在测试中控制模拟的Db
this.connect显示了如何返回一个promise以及如何在需要时模拟连接到DB的错误
collection :( name)=> {确保您对createUser和测试的调用可以获得相同的集合接口并检查是否实际调用了模拟函数。
insert:jest.fn()。mockImplementation((data)=> {显示如何通过创建自己的实现来返回数据
const ObjectID = require.requireActual('mongodb')。ObjectID;展示了如何在早先模拟mongodb时获取实际的模块对象
Now comes the testing part. This is the updated user.test.js
现在是测试部分。这是更新的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 loaded -
let user = await createUser({}, {
since you are usingasync
, you will not usethen
orcatch
. That is the point of usingasync
function. -
db.__connectError(true);
will set the global variableconnected
tofalse
andconnectError
to true. So whencreateUser
gets called in the test it will simulate a connection error -
ex= err;
, see how I capture the exception and take out the expect call. If you do expect in thecatch
block itself, then when an exception is not raised the test will still pass. That is why I have done exception testing outside thetry/catch
block
jest.mock( '../../ LIB /分贝');将确保我们的手动模拟加载
让user = await createUser({},{因为你使用异步,你不会使用then或catch。这就是使用异步函数的重点。
分贝.__ connectError(真);将全局变量设置为false并将connectError设置为true。因此,当在测试中调用createUser时,它将模拟连接错误
ex = err;,看看我如何捕获异常并取出期望的调用。如果你确实期望在catch块本身,那么当没有引发异常时,测试仍然会通过。这就是我在try / catch块之外进行异常测试的原因
Now comes the part of testing it by running npm test
and we get
现在是通过运行npm测试来测试它的部分,我们得到了
All of it is committed to below repo you shared
所有这些都致力于您共享的以下回购
#2
1
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.
您正在对DB的实例进行存根,而不是实际的DB类。另外,我没有在代码中看到db.usersInsert方法。我们不能为您编写代码,但我可以指出您正确的方向。此外,我不使用Jest,但Sinon的概念是相同的。在你的情况下做的最好的事情我相信是存在返回你正在与之交互的对象的类方法的原型。
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);
});
});
#1
2
There are multiple ways to go about this. I will list few of them
有很多方法可以解决这个问题。我会列出其中的一些
- Mock the DB class using a Jest manual mock. This could be cumbersome if you are using too many mongo functions. But since you are encapsulating most through the DB class it may still be manageable
- Use a mocked mongo instance. This project allows you to simulate a MongoDB and persist data using js file
- Use a in-memory mongodb
- Use a actual mongodb
使用Jest手动模拟来模拟DB类。如果你使用太多的mongo函数,这可能很麻烦。但是,由于您通过数据库类进行封装,因此它仍然可以管理
使用模拟的mongo实例。此项目允许您使用js文件模拟MongoDB并保留数据
使用内存中的mongodb
使用实际的mongodb
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
我将在这里展示第一个案例,您发布了代码以及如何使其工作。所以我们要做的第一件事是将__mocks __ / db.js文件更新到下面
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 mocked - The
connected
,connectError
,mock_collections
are global variables. This is so that we can impact the state of theDb
that youruser.js
loads. If we don't do this, we won't be able to control the mockedDb
from within our tests -
this.connect
shows how you can return a promise and also how you can simulate a error connecting to DB when you want -
collection: (name) => {
makes sure that your call tocreateUser
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 implementation -
const ObjectID = require.requireActual('mongodb').ObjectID;
shows how you can get an actual module object when you have already mockedmongodb
earlier
jest.mock( 'mongodb的');将确保任何实际的mongodb调用被嘲笑
connected,connectError,mock_collections是全局变量。这样我们就可以影响你的user.js加载的Db的状态。如果我们不这样做,我们将无法在测试中控制模拟的Db
this.connect显示了如何返回一个promise以及如何在需要时模拟连接到DB的错误
collection :( name)=> {确保您对createUser和测试的调用可以获得相同的集合接口并检查是否实际调用了模拟函数。
insert:jest.fn()。mockImplementation((data)=> {显示如何通过创建自己的实现来返回数据
const ObjectID = require.requireActual('mongodb')。ObjectID;展示了如何在早先模拟mongodb时获取实际的模块对象
Now comes the testing part. This is the updated user.test.js
现在是测试部分。这是更新的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 loaded -
let user = await createUser({}, {
since you are usingasync
, you will not usethen
orcatch
. That is the point of usingasync
function. -
db.__connectError(true);
will set the global variableconnected
tofalse
andconnectError
to true. So whencreateUser
gets called in the test it will simulate a connection error -
ex= err;
, see how I capture the exception and take out the expect call. If you do expect in thecatch
block itself, then when an exception is not raised the test will still pass. That is why I have done exception testing outside thetry/catch
block
jest.mock( '../../ LIB /分贝');将确保我们的手动模拟加载
让user = await createUser({},{因为你使用异步,你不会使用then或catch。这就是使用异步函数的重点。
分贝.__ connectError(真);将全局变量设置为false并将connectError设置为true。因此,当在测试中调用createUser时,它将模拟连接错误
ex = err;,看看我如何捕获异常并取出期望的调用。如果你确实期望在catch块本身,那么当没有引发异常时,测试仍然会通过。这就是我在try / catch块之外进行异常测试的原因
Now comes the part of testing it by running npm test
and we get
现在是通过运行npm测试来测试它的部分,我们得到了
All of it is committed to below repo you shared
所有这些都致力于您共享的以下回购
#2
1
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.
您正在对DB的实例进行存根,而不是实际的DB类。另外,我没有在代码中看到db.usersInsert方法。我们不能为您编写代码,但我可以指出您正确的方向。此外,我不使用Jest,但Sinon的概念是相同的。在你的情况下做的最好的事情我相信是存在返回你正在与之交互的对象的类方法的原型。
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);
});
});