如何为mongoDB方法模拟动态? [英] How to mock dynamical for mongoDB method?

查看:77
本文介绍了如何为mongoDB方法模拟动态?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In my application code there are several places, where I have to connect to a DB and get some data. For my unit tests (I'm using JestJS), I need to mock this out.

让我们假设这个简单的异步功能:

Let's assume this simple async function:

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

数据库连接位于单独的文件中

The DB connection is in a separate file:

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

您可以看到,我将收到数据库并获得一个集合.之后,我将接收数据.

You can see, I'll recieve the DB and get a collection. After this I will recieve the data.

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

My attempt for the mock so far:

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

也许这不是最好的方法...?我为每一项改进感到非常高兴.

Maybe this is not the best way to do it...? I'm very happy for every improvement.

我的问题是,因为有多个测试,并且findOne()调用的每个测试都需要一个不同的模拟结果,所以将数据库模拟放置在哪里.

My question is where to put the DB mock as there are multiple tests and every test needs a different mock result for the findOne() call.

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

Maybe it is possible to create a function, which gets called with the needed parameter or something like that.

推荐答案

首先,我只想指出,按原样测试此概念验证功能的价值很小.那里实际上并没有任何代码.都是对数据库客户端的调用.该测试基本上是在验证是否模拟数据库客户端以返回null时,它是否返回null.因此,您实际上只是在测试您的模拟游戏.

First I just want to note that testing this proof-of-concept function as-is appears low in value. There isn't really any of your code in there; it's all calls to the DB client. The test is basically verifying that, if you mock the DB client to return null, it returns null. So you're really just testing your mock.

但是,如果您的函数在返回数据之前以某种方式转换了数据,则将很有用. (尽管在那种情况下,我会通过自己的测试将转换放入其自己的函数中,而将我们留在开始的地方.)

However, it would be useful if your function transformed the data somehow before returning it. (Although in that case I would put the transform in its own function with its own tests, leaving us back where we started.)

因此,我将建议一种解决方案,该解决方案可以完成您要求的,然后有望改善您的代码.

So I'll suggest a solution that does what you asked, and then one that will hopefully improve your code.

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

You can create a function that returns a mock that provides a findOne() that returns whatever you specify:

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

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

Then in your code file, call DB.getDB.mockImplementation in beforeEach or beforeAll above each test, passing in the desired return value, like this:

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

一种解决方案,可以简化与数据库相关的代码的测试

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

Solution that makes testing much easier across your DB-related code

This question is really exciting, because it is a wonderful illustration of the value of having each function do only one thing!

getData看起来很小-只有3行加上return语句.因此乍看之下似乎并没有做太多.

getData appears to be very small - only 3 lines plus a return statement. So at first glance it doesn't seem to be doing too much.

但是,这个微小的功能与DB的内部结构紧密结合.它依赖于:

However, this tiny function has very tight coupling with the internal structure of DB. It has dependency on:

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

这会带来一些负面影响:

This has some negative repercussions:

  1. 如果DB曾经改变其结构(由于它使用了第3方组件)是可能的,那么具有这些依赖关系的每个功能都会中断.
  2. 很难测试!
  3. 该代码不可重用.因此,访问数据库的每个函数都需要调用getDB()db.get('collection'),从而导致代码重复.
  1. If DB ever changes its structure, which since it uses a 3rd party component, is possible, then every function you have that has these dependencies will break.
  2. It's very hard to test!
  3. The code isn't reusable. So every function that accesses the DB will need to call getDB() and db.get('collection'), resulting in repeated code.

这是一种可以改进功能,同时使测试模拟更加简单的方法.

Here's one way you could improve things, while making your test mocks much simpler.

我可能是错的,但是我的猜测是,每次您使用DB时,您要做的第一件事就是调用getDB().但是您只需要在整个代码库中进行一次调用即可.您可以从./lib/db.js而不是DB导出db,而不是在所有位置重复该代码:

I could be wrong, but my guess is, every time you use DB, the first thing you'll do is call getDB(). But you only ever need to make that call once in your entire codebase. Instead of repeating that code everywhere, you can export db from ./lib/db.js instead of DB:

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

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

Alternatively, you could create the db instance in a startup function and then pass it in to a DataAccessLayer class, which would house all of your DB access calls. Again only calling getDB() once. That way you avoid the singleton, which makes testing easier because it allows dependency injection.

// ./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;

此功能非常简单,似乎没有必要.毕竟,它与要替换的代码具有相同的行数!但这会从调用代码中消除对dbInstance(以前为db)结构的依赖性,同时记录get()的工作(从名称上并不明显).

This function is so trivial it might seem unnecessary. After all, it has the same number of lines as the code it replaces! But it removes the dependency on the structure of dbInstance (previously db) from calling code, while documenting what get() does (which is not obvious from its name).

现在您要重新命名getDocById以反映其实际功能的getData看起来像这样:

Now your getData, which I'm renaming getDocById to reflect what it actually does, can look like this:

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

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

现在,您可以与DB分开单独模拟getCollectionByName:

Now you can mock getCollectionByName separately from 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();
  });
});

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

This is just one approach and it could be taken much further. For example we could export findOneDocById(collectionName, id) and/or findOneDoc(collectionName, searchObject) to make both our mock and calls to findOne() simpler.

这篇关于如何为mongoDB方法模拟动态?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆