Как создать макет класса для подключения к mongoDB?

Я попытался создать класс для подключения к mongoDB (и получить соединение gridFS с помощью (gridfs-stream). Но с этим у меня возникают две проблемы:

  1. Я иногда получаю ошибку mongo server instance in invalid state connected
  2. для меня невозможно издеваться над этим классом - используя jestJS

поэтому я был бы очень благодарен, если кто-то может помочь мне оптимизировать этот класс, чтобы получить действительно солидный рабочий класс. Например мне не нравится let that = this в подключение() функция.

пример РЕПО

класс DB

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
  }
}

пример

эта функция добавит нового пользователя в БД, используя класс выше:

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()
  })
}

единица теста

мне нужно создать модульный тест, чтобы проверить, вызван ли метод mongoDB. Нет интеграционного теста для тестирования метода. Поэтому мне нужно издеваться над подключением, коллекцией и вставкой DB метод.

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 ответов


существует несколько способов сделать это. Я перечислю несколько из них

  • издевайтесь над классом DB, используя ручной макет шутки. Это может быть громоздким, если вы используете слишком много функций монго. Но поскольку вы инкапсулируете большинство через класс DB, он все еще может быть управляемым
  • используйте издевательский экземпляр mongo. этой проект позволяет моделировать MongoDB и сохранять данные с помощью JS-файла
  • использовать в памяти в MongoDB
  • используйте фактический mongodb

я продемонстрирую первый случай здесь, который вы опубликовали с кодом и как заставить его работать. Поэтому первое, что мы сделаем, это обновим __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];
            }
        }
    }

}

теперь несколько пояснений

  • jest.mock('mongodb'); убедитесь, что любой фактический вызов mongodb высмеивается
  • на connected, connectError, mock_collections глобальные переменные. Это для того, чтобы мы могли повлиять на состояние Db ваш user.js нагрузки. Если мы этого не сделаем, мы не сможем контролировать издевательства Db из наших тестов
  • this.connect показывает, как вы можете вернуть обещание, а также как вы можете имитировать ошибку при подключении к БД, когда хотите
  • collection: (name) => { убедитесь, что ваш вызов createUser и ваш тест может получить тот же интерфейс коллекции и проверить, были ли фактически вызваны издевательские функции.
  • insert: jest.fn().mockImplementation((data) => { показывает, как вы можете вернуть данные создание собственной реализации
  • const ObjectID = require.requireActual('mongodb').ObjectID; показывает, как вы можете получить фактический объект модуля, когда вы уже насмеялись mongodb ранее

теперь идет часть тестирования. Это обновленный 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");
    })
})

несколько советов снова

  • jest.mock('../../lib/db'); убедитесь, что наш ручной макет загружается
  • let user = await createUser({}, { если вы используете async, вы не будете использовать then или catch. В этом суть используя

вы заглушаете экземпляр DB, а не фактический класс DB. Кроме того, я не вижу db.usersInsert метод в коде. Мы не можем написать ваш код, но я могу указать вам правильное направление. Кроме того, я не использую шутку, но концепции Синон одинаковы. Лучшее, что можно сделать в вашем случае, я считаю, это заглушить прототип метода класса, который возвращает объект, с которым вы взаимодействуете.

что-то вроде этого:

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