如何创建一个可模拟的类来连接到mongoDB?

时间:2021-05-19 03:01:49

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连接。但是我确实遇到了两个问题:

  1. I do get sometimes the mongo Error server instance in invalid state connected
  2. 我确实有时连接无效状态的mongo错误服务器实例

  3. It is impossible for me to mock this class out - using jestJS
  4. 我不可能使用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。

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:

此函数将使用上面的类向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
  • 使用Jest手动模拟来模拟DB类。如果你使用太多的mongo函数,这可能很麻烦。但是,由于您通过数据库类进行封装,因此它仍然可以管理

  • Use a mocked mongo instance. This project allows you to simulate a MongoDB and persist data using js file
  • 使用模拟的mongo实例。此项目允许您使用js文件模拟MongoDB并保留数据

  • Use a in-memory mongodb
  • 使用内存中的mongodb

  • Use a actual 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
  • jest.mock( 'mongodb的');将确保任何实际的mongodb调用被嘲笑

  • The connected, 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 tests
  • connected,connectError,mock_collections是全局变量。这样我们就可以影响你的user.js加载的Db的状态。如果我们不这样做,我们将无法在测试中控制模拟的Db

  • this.connect shows how you can return a promise and also how you can simulate a error connecting to DB when you want
  • this.connect显示了如何返回一个promise以及如何在需要时模拟连接到DB的错误

  • collection: (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.
  • collection :( name)=> {确保您对createUser和测试的调用可以获得相同的集合接口并检查是否实际调用了模拟函数。

  • insert: jest.fn().mockImplementation((data) => { shows how you can return data by creating your own implementation
  • insert:jest.fn()。mockImplementation((data)=> {显示如何通过创建自己的实现来返回数据

  • const ObjectID = require.requireActual('mongodb').ObjectID; shows how you can get an actual module object when you have already mocked mongodb earlier
  • 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
  • jest.mock( '../../ LIB /分贝');将确保我们的手动模拟加载

  • let user = await createUser({}, { since you are using async, you will not use then or catch. That is the point of using async function.
  • 让user = await createUser({},{因为你使用异步,你不会使用then或catch。这就是使用异步函数的重点。

  • 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 error
  • 分贝.__ connectError(真);将全局变量设置为false并将connectError设置为true。因此,当在测试中调用createUser时,它将模拟连接错误

  • ex= 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 block
  • ex = err;,看看我如何捕获异常并取出期望的调用。如果你确实期望在catch块本身,那么当没有引发异常时,测试仍然会通过。这就是我在try / catch块之外进行异常测试的原因

Now comes the part of testing it by running npm test and we get

现在是通过运行npm测试来测试它的部分,我们得到了

如何创建一个可模拟的类来连接到mongoDB?

All of it is committed to below repo you shared

所有这些都致力于您共享的以下回购

https://github.com/jaqua/mongodb-class

#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
  • 使用Jest手动模拟来模拟DB类。如果你使用太多的mongo函数,这可能很麻烦。但是,由于您通过数据库类进行封装,因此它仍然可以管理

  • Use a mocked mongo instance. This project allows you to simulate a MongoDB and persist data using js file
  • 使用模拟的mongo实例。此项目允许您使用js文件模拟MongoDB并保留数据

  • Use a in-memory mongodb
  • 使用内存中的mongodb

  • Use a actual 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
  • jest.mock( 'mongodb的');将确保任何实际的mongodb调用被嘲笑

  • The connected, 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 tests
  • connected,connectError,mock_collections是全局变量。这样我们就可以影响你的user.js加载的Db的状态。如果我们不这样做,我们将无法在测试中控制模拟的Db

  • this.connect shows how you can return a promise and also how you can simulate a error connecting to DB when you want
  • this.connect显示了如何返回一个promise以及如何在需要时模拟连接到DB的错误

  • collection: (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.
  • collection :( name)=> {确保您对createUser和测试的调用可以获得相同的集合接口并检查是否实际调用了模拟函数。

  • insert: jest.fn().mockImplementation((data) => { shows how you can return data by creating your own implementation
  • insert:jest.fn()。mockImplementation((data)=> {显示如何通过创建自己的实现来返回数据

  • const ObjectID = require.requireActual('mongodb').ObjectID; shows how you can get an actual module object when you have already mocked mongodb earlier
  • 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
  • jest.mock( '../../ LIB /分贝');将确保我们的手动模拟加载

  • let user = await createUser({}, { since you are using async, you will not use then or catch. That is the point of using async function.
  • 让user = await createUser({},{因为你使用异步,你不会使用then或catch。这就是使用异步函数的重点。

  • 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 error
  • 分贝.__ connectError(真);将全局变量设置为false并将connectError设置为true。因此,当在测试中调用createUser时,它将模拟连接错误

  • ex= 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 block
  • ex = err;,看看我如何捕获异常并取出期望的调用。如果你确实期望在catch块本身,那么当没有引发异常时,测试仍然会通过。这就是我在try / catch块之外进行异常测试的原因

Now comes the part of testing it by running npm test and we get

现在是通过运行npm测试来测试它的部分,我们得到了

如何创建一个可模拟的类来连接到mongoDB?

All of it is committed to below repo you shared

所有这些都致力于您共享的以下回购

https://github.com/jaqua/mongodb-class

#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);
  });
});