草庐IT

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

coder 2024-07-25 原文

我已经尝试创建一个类来连接到 mongoDB(并使用 (gridfs-stream) 获得 gridFS 连接)。但是我确实遇到了两个问题:

  1. 我有时会收到 mongo 错误 server instance in invalid state connected
  2. 我不可能模拟这个类 - 使用 jestJS

所以如果有人可以帮助我优化这个类以获得一个真正扎实的工作类,我将非常感激。例如,我不喜欢 connect() 函数中的 let that = this

Example repo

数据库类

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 方法。没有用于测试该方法的集成测试。 所以我需要模拟数据库连接、收集和插入方法。

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

最佳答案

有多种方法可以解决这个问题。我将列出其中的一些

  • 使用 Jest 手动模拟模拟数据库类。如果您使用太多 mongo 函数,这可能会很麻烦。但是由于您是通过 DB 类封装大部分内容,因此它可能仍然是可管理的
  • 使用模拟的 mongo 实例。 This项目允许您模拟 MongoDB 并使用 js 文件持久化数据
  • 使用 in-memory数据库
  • 使用实际的 mongodb

我将在此处展示第一个案例,您发布了该案例以及代码以及如何使其发挥作用。所以我们要做的第一件事是更新 __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];
            }
        }
    }

}

现在很少解释

  • jest.mock('mongodb');将确保任何实际的 mongodb 调用都被模拟
  • connected , connectError , mock_collections是全局变量。这样我们就可以影响 Db 的状态你的user.js负载。如果我们不这样做,我们将无法控制模拟的 Db。来 self 们的测试
  • this.connect展示了如何返回一个 promise ,以及如何在需要时模拟连接到数据库的错误
  • collection: (name) => {确保您调用 createUser并且您的测试可以获得相同的集合接口(interface)并检查是否实际调用了模拟函数。
  • 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 , 你不会使用 thencatch .这就是使用 async 的意义所在功能。
  • db.__connectError(true);将设置全局变量 connectedfalseconnectError为真。所以当createUser在测试中被调用它将模拟连接错误
  • ex= err; ,看看我是如何捕获异常并取出 expect 调用的。如果您确实希望在 catch block 本身,那么当没有引发异常时,测试仍然会通过。这就是为什么我在 try/catch 之外进行异常测试的原因阻止

现在是通过运行 npm test 对其进行测试的部分我们得到

所有这些都提交给您共享的以下 repo

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

关于javascript - 如何创建一个可模拟的类来连接到 mongoDB?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49345702/

有关javascript - 如何创建一个可模拟的类来连接到 mongoDB?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  4. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  5. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  10. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

随机推荐