【问题标题】:How to connect to mongoDB and test for drop collection?如何连接到 mongoDB 并测试 drop collection?
【发布时间】:2018-06-26 04:11:59
【问题描述】:

这就是我使用monk() 连接到 mongoDB 的方式。我会把它存储在state

假设我们要删除一些集合,我们调用dropDB

db.js

var state = {
  db: null
}

export function connection () {
  if (state.db) return
  state.db = monk('mongdb://localhost:27017/db')
  return state.db
}

export async function dropDB () {
  var db = state.db
  if (!db) throw Error('Missing database connection')

  const Users = db.get('users')
  const Content = db.get('content')

  await Users.remove({})
  await Content.remove({})
}

我不太确定使用state 变量是否是一种好方法。也许有人可以对此发表评论或显示改进。

现在我想用 JestJS 为这个函数写一个单元测试:

db.test.js

import monk from 'monk'
import { connection, dropDB } from './db'
jest.mock('monk')

describe('dropDB()', () => {
  test('should throw error if db connection is missing', async () => {
    expect.assertions(1)
    await expect(dropDB()).rejects.toEqual(Error('Missing database connection'))
  })
})

这部分很简单,但是下一部分给我带来了两个问题:

如何模拟remove() 方法?

  test('should call remove() methods', async () => {
    connection() // should set `state.db`, but doesn't work
    const remove = jest.fn(() => Promise.resolve({ n: 1, nRemoved: 1, ok: 1 }))
    // How do I use this mocked remove()?
    expect(remove).toHaveBeenCalledTimes(2)
  })

在那之前呢?如何设置state.db


更新

正如 poke 所解释的,全局变量会造成问题。于是我换了一个班:

db.js

export class Db {
  constructor() {
    this.connection = monk('mongdb://localhost:27017/db');
  }

  async dropDB() {
    const Users = this.connection.get('users');
    const Content = this.connection.get('content');

    await Users.remove({});
    await Content.remove({});
  }
}

这会产生这个测试文件:

db.test.js

import { Db } from './db'
jest.mock('./db')

let db
let remove

describe('DB class', () => {
  beforeAll(() => {
    const remove = jest.fn(() => Promise.resolve({ n: 1, nRemoved: 1, ok: 1 }))
    Db.mockImplementation(() => {
      return { dropDB: () => {
        // Define this.connection.get() and use remove as a result of it
      } }
    })
  })
  describe('dropDB()', () => {
    test('should call remove method', () => {
      db = new Db()
      db.dropDB()
      expect(remove).toHaveBeenCalledTimes(2)
    })
  })
})

如何模拟任何this 元素?在这种情况下,我需要模拟 this.connection.get()

【问题讨论】:

  • 您在呼叫连接吗?我没有看到“连接”的调用。

标签: javascript node.js mongodb unit-testing jestjs


【解决方案1】:

拥有全局状态绝对是您问题的根源。我建议寻找一个根本不涉及全局变量的解决方案。根据Global Variables Are Bad,全局变量会导致紧密耦合并使测试变得困难(正如您自己注意到的那样)。

更好的解决方案是要么将数据库连接显式传递给dropDB 函数,因此它将连接作为显式依赖,或者引入一些保持连接的有状态对象并提供dropDB 作为方法。

第一个选项如下所示:

export function openConnection() {
  return monk('mongdb://localhost:27017/db');
}

export async function dropDB(connection) {
  if (!connection) {
    throw Error('Missing database connection');
  }

  const Users = connection.get('users');
  const Content = connection.get('content');

  await Users.remove({});
  await Content.remove({});
}

这也使得测试dropDB 变得非常容易,因为您现在可以直接为它传递一个模拟对象。

另一个选项可能如下所示:

export class Connection() {
  constructor() {
    this.connection = monk('mongdb://localhost:27017/db');
  }

  async dropDB() {
    const Users = this.connection.get('users');
    const Content = this.connection.get('content');

    await Users.remove({});
    await Content.remove({});
  }
}

第一个选项的测试可能如下所示:

test('should call remove() methods', async () => {
  const usersRemove = jest.fn().mockReturnValue(Promise.resolve(null));
  const contentRemove = jest.fn().mockReturnValue(Promise.resolve(null));
  const dbMock = {
    get(type) {
      if (type === 'users') {
        return { remove: usersRemove };
      }
      else if (type === 'content') {
        return { remove: contentRemove };
      }
    }
  };

  await dropDB(dbMock);

  expect(usersRemove).toHaveBeenCalledTimes(1);
  expect(contentRemove).toHaveBeenCalledTimes(1);
});

基本上,dropDB 函数需要一个具有 get 方法的对象,该对象在调用时返回一个具有 remove 方法的对象。所以你只需要传递 something 看起来像那样,这样函数就可以调用那些remove 方法。


对于类,这有点复杂,因为构造函数依赖于monk 模块。一种方法是再次明确该依赖项(就像在第一个解决方案中一样),并在那里传递monk 或其他一些工厂。但是我们也可以使用Jest’s manual mocks 来简单地模拟整个monk 模块。

请注意,我们确实想模拟包含Connection 类型的模块。我们想测试它,所以我们需要它处于未模拟状态。

要模拟monk,我们需要在__mocks__/monk.js 创建一个模拟模块。手册指出这个__mocks__文件夹应该和node_modules文件夹相邻。

在该文件中,我们只需导出自定义的monk 函数。这与我们在第一个示例中已经使用的几乎相同,因为我们只关心获得那些 remove 方法:

export default function mockedMonk (url) {
  return {
    get(type) {
      if (type === 'users') {
        return { remove: mockedMonk.usersRemove };
      }
      else if (type === 'content') {
        return { remove: mockedMonk.contentRemove };
      }
    }
  };
};

请注意,这指的是mockedMonk.usersRemovemockedMonk.contentRemove 等函数。我们将在测试中使用它来在测试执行期间显式配置这些功能。

现在,在测试函数中,我们需要调用 jest.mock('monk') 以使 Jest 能够使用我们模拟的模块模拟​​ monk 模块。然后,我们也可以导入它并在测试中设置我们的功能。基本上和上面一样:

import { Connection } from './db';
import monk from 'monk';

// enable mock
jest.mock('./monk');

test('should call remove() methods', async () => {
  monk.usersRemove = jest.fn().mockReturnValue(Promise.resolve(null));
  monk.contentRemove = jest.fn().mockReturnValue(Promise.resolve(null));

  const connection = new Connection();
  await connection.dropDB();

  expect(monk.usersRemove).toHaveBeenCalledTimes(1);
  expect(monk.contentRemove).toHaveBeenCalledTimes(1);
});

【讨论】:

  • 我为第一个解决方案添加了一个示例测试。我以前没有真正使用过 Jest,所以我不知道这是否是最好的方法。
  • 我已经更新了帖子,因为我决定使用类切换到您的解决方案。但是我一直在嘲笑任何 this 实例。你能告诉我如何得到这个吗?我可以完全模拟它还是必须通过模拟 monk() 来间接地模拟它?
  • @user3142695 用基于类的解决方案的工作测试更新了帖子。
  • 非常感谢您的精彩解释。这对我了解更多细节很有帮助。
猜你喜欢
  • 2023-03-11
  • 1970-01-01
  • 2013-09-01
  • 2012-01-22
  • 2021-12-20
  • 1970-01-01
  • 2020-07-04
  • 2017-05-24
  • 1970-01-01
相关资源
最近更新 更多