如何为 mongoDB 方法模拟动态?

use*_*695 1 javascript unit-testing jestjs

在我的应用程序代码中有几个地方,我必须连接到数据库并获取一些数据。对于我的单元测试(我正在使用 JestJS),我需要对此进行模拟。

让我们假设这个简单的异步函数:

/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
}
Run Code Online (Sandbox Code Playgroud)

数据库连接在一个单独的文件中:

/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
}
Run Code Online (Sandbox Code Playgroud)

你可以看到,我会收到数据库并收集。在此之后,我将收到数据。

到目前为止,我对模拟的尝试:

/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()
  })
})
Run Code Online (Sandbox Code Playgroud)

也许这不是最好的方法......?我对每一次改进都感到非常高兴。

我的问题是将 DB 模拟放在哪里,因为有多个测试,每个测试都需要不同的findOne()调用模拟结果。

也许可以创建一个函数,该函数使用所需的参数或类似的参数进行调用。

sto*_*one 6

首先,我只想指出,按原样测试这个概念验证功能的价值似乎很低。那里没有你的任何代码;这都是对数据库客户端的调用。该测试基本上是在验证,如果您模拟 DB 客户端返回 null,则它返回 null。所以你真的只是在测试你的模拟。

但是,如果您的函数在返回数据之前以某种方式转换了数据,那将会很有用。(尽管在这种情况下,我会将变换放在自己的函数中并进行自己的测试,让我们回到开始的地方。)

因此,我会建议一个解决方案来满足您的要求,然后是一个有望改进您的代码的解决方案。

不需要更改的解决方案getData()- 不推荐:

您可以创建一个返回模拟的函数,该模拟提供findOne()返回您指定的任何内容的模拟:

// ./db-test-utils
function makeMockGetDbWithFindOneThatReturns(returnValue) {
  const findOne = jest.fn(() => Promise.resolve(returnValue));
  return jest.fn(() => ({
    get: () => ({ findOne })
  }));
}
Run Code Online (Sandbox Code Playgroud)

然后在您的代码文件中,DB.getDB.mockImplementation在每个测试上方调用beforeEach 或 beforeAll,传入所需的返回值,如下所示:

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();
  });
});
Run Code Online (Sandbox Code Playgroud)

使跨数据库相关代码的测试变得更加容易的解决方案

这个问题真的很令人兴奋,因为它很好地说明了让每个函数只做一件事的价值!

getData看起来很小——只有 3 行加上一条return语句。所以乍一看它似乎并没有做太多。

然而,这个微小的函数与 的内部结构有着非常紧密的耦合DB。它依赖于:

  • DB - 单身人士
  • DB.getDB()
  • DB.getDB().get()
  • DB.getDB().get().findOne()

这有一些负面影响:

  1. 如果DB更改其结构(因为它使用 3rd 方组件)是可能的,那么您拥有的每个具有这些依赖项的功能都会中断。
  2. 很难测试!
  3. 代码不可重用。所以每个访问数据库的函数都需要调用getDB()db.get('collection'),导致代码重复。

这是您可以改进事物的一种方法,同时使您的测试模拟更加简单。

导出db而不是DB

我可能是错的,但我的猜测是,每次使用 时DB,您要做的第一件事就是调用getDB()。但是您只需要在整个代码库中进行一次该调用。您可以db从而./lib/db.js不是在任何地方重复该代码,而不是DB

// ./lib/db.js
const DB = existingCode(); // However you're creating DB now
const dbInstance = DB.getDB();
export default dbInstance;
Run Code Online (Sandbox Code Playgroud)

或者,您可以在启动函数中创建 db 实例,然后将其传递给 DataAccessLayer 类,该类将容纳您的所有数据库访问调用。再次只调用getDB()一次。这样你就避免了单例,这使得测试更容易,因为它允许依赖注入。

添加辅助函数获取DB集合

// ./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;
Run Code Online (Sandbox Code Playgroud)

这个函数非常微不足道,似乎没有必要。毕竟,它的行数与其替换的代码行数相同!但是它从调用代码中消除了对dbInstance(以前的 db)结构的依赖,同时记录了get()它的作用(这从它的名字中看不出来)。

现在你的getData,我正在重命名getDocById以反映它的实际作用,它看起来像这样:

import { getCollectionByName } from './lib/db';

export async function getDocById(id) {
  const collection = getCollectionByName('things');
  const doc = await collection.findOne({ _id: id })
  return doc;
}
Run Code Online (Sandbox Code Playgroud)

现在您可以将 getCollectionByName 与 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();
  });
});
Run Code Online (Sandbox Code Playgroud)

这只是一种方法,可以采取更进一步的方法。例如,我们可以导出findOneDocById(collectionName, id)和/或findOneDoc(collectionName, searchObject)使我们的模拟和调用findOne()更简单。