Mocking/Stubbing Mongoose 模型保存方法

2024-04-29

给定一个简单的 Mongoose 模型:

import mongoose, { Schema } from 'mongoose';

const PostSchema = Schema({
  title:    { type: String },
  postDate: { type: Date, default: Date.now }
}, { timestamps: true });

const Post = mongoose.model('Post', PostSchema);

export default Post;

我想测试这个模型,但遇到了一些障碍。

我当前的规格看起来像这样(为简洁起见,省略了一些内容):

import mongoose from 'mongoose';
import { expect } from 'chai';
import { Post } from '../../app/models';

describe('Post', () => {
  beforeEach((done) => {
    mongoose.connect('mongodb://localhost/node-test');
    done();
  });

  describe('Given a valid post', () => {
    it('should create the post', (done) => {
      const post = new Post({
        title: 'My test post',
        postDate: Date.now()
      });

      post.save((err, doc) => {
        expect(doc.title).to.equal(post.title)
        expect(doc.postDate).to.equal(post.postDate);
        done();
      });
    });
  });
});

然而,这样我每次运行测试时都会访问我的数据库,这是我希望避免的。

我尝试过使用仿鹅 https://github.com/mccormicka/Mockgoose/,但是我的测试将无法运行。

import mockgoose from 'mockgoose';
// in before or beforeEach
mockgoose(mongoose);

测试卡住并抛出错误:Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.我尝试将超时时间增加到 20 秒,但这并没有解决任何问题。

接下来,我扔掉了 Mockgoose 并尝试使用 Sinon 来打断save call.

describe('Given a valid post', () => {
  it('should create the post', (done) => {
    const post = new Post({
      title: 'My test post',
      postDate: Date.now()
    });

    const stub = sinon.stub(post, 'save', function(cb) { cb(null) })
    post.save((err, post) => {
      expect(stub).to.have.been.called;
      done();
    });
  });
});

这个测试通过了,但对我来说没有多大意义。我对存根、嘲笑等还很陌生,……我不确定这是否是正确的方法。我正在存根save方法上post,然后我断言它已被调用,但我显然是在调用它...另外,我似乎无法获取非存根 Mongoose 方法将返回的参数。我想比较一下post变量为某事save方法返回,就像我访问数据库的第一个测试一样。我尝试过一个couple https://stackoverflow.com/questions/22340419/stub-save-instance-method-of-mongoose-model-with-sinon of methods https://stackoverflow.com/questions/11318972/stubbing-a-mongoose-model-with-sinon但他们都感觉很hackish。一定有一个干净的方法,不是吗?

有几个问题:

  • 我确实应该避免像我总是到处阅读一样访问数据库吗?我的第一个示例运行良好,每次运行后我都可以清除数据库。然而,我真的感觉不太对劲。

  • 我如何从 Mongoose 模型中存根 save 方法并确保它实际测试我想要测试的内容:将新对象保存到数据库。


基础

在单元测试中,不应访问数据库。我可以想到一个例外:访问内存中的数据库,但即使这已经属于集成测试的范围,因为您只需要为复杂的进程保存在内存中的状态(因此不是真正的功能单元)。所以,是的,没有实际的数据库。

您想要在单元测试中测试的是您的业务逻辑在应用程序和数据库之间的接口处产生正确的 API 调用。您可以而且可能应该假设数据库 API/驱动程序开发人员已经完成了良好的测试,确保 API 下面的所有内容都按预期运行。但是,您还希望在测试中涵盖业务逻辑如何对不同的有效 API 结果做出反应,例如成功保存、由于数据一致性而失败、由于连接问题而失败等。

这意味着您需要并想要模拟的是数据库驱动程序接口下方的所有内容。但是,您需要对该行为进行建模,以便可以针对数据库调用的所有结果测试您的业务逻辑。

说起来容易做起来难,因为这意味着您需要通过您使用的技术访问 API,并且您需要了解 API。

猫鼬的现实

坚持基础知识,我们想要模拟 mongoose 使用的底层“驱动程序”执行的调用。假设是节点 mongodb-native https://github.com/mongodb/node-mongodb-native我们需要模拟这些调用。理解 mongoose 和本机驱动程序之间的完整相互作用并不容易,但它通常归结为以下方法:mongoose.Collection因为后者延伸了mongoldb.Collection and does not重新实现方法,例如insert。如果我们能够控制人的行为insert在这种特殊情况下,我们知道我们在 API 级别模拟了数据库访问。您可以在两个项目的源代码中追踪它,即Collection.insert确实是本机驱动程序方法。

对于我创建的特定示例公共 Git 存储库 https://code.teris.io/parked/mongoose-mock-example具有完整的包,但我将在答案中发布所有元素。

解决方案

就我个人而言,我发现使用 mongoose 的“推荐”方式非常不可用:模型通常在定义相应模式的模块中创建,但它们已经需要连接。为了在同一个项目中拥有多个连接来与完全不同的 mongodb 数据库进行通信以及为了测试目的,这让生活变得非常困难。事实上,一旦关注点完全分离,猫鼬,至少对我来说,就变得几乎无法使用。

所以我创建的第一件事是包描述文件,一个带有模式的模块和一个通用的“模型生成器”:

包.json

{
  "name": "xxx",
  "version": "0.1.0",
  "private": true,
  "main": "./src",
  "scripts": {
    "test" : "mocha --recursive"
  },
  "dependencies": {
    "mongoose": "*"
  },
  "devDependencies": {
    "mocha": "*",
    "chai": "*"
  }
}

src/post.js

var mongoose = require("mongoose");

var PostSchema = new mongoose.Schema({
    title: { type: String },
    postDate: { type: Date, default: Date.now }
}, {
    timestamps: true
});

module.exports = PostSchema;

src/index.js

var model = function(conn, schema, name) {
    var res = conn.models[name];
    return res || conn.model.bind(conn)(name, schema);
};

module.exports = {
    PostSchema: require("./post"),
    model: model
};

这样的模型生成器有其缺点:可能需要将某些元素附加到模型,并且将它们放置在创建模式的同一模块中是有意义的。因此,找到一种通用的方法来添加这些内容有点棘手。例如,模块可以导出后操作,以便在为给定连接生成模型时自动运行等(黑客攻击)。

现在让我们模拟 API。我会保持简单,只会模拟相关测试所需的内容。重要的是,我想模拟一般的 API,而不是单个实例的单个方法。后者在某些情况下可能很有用,或者当没有其他帮助时,但我需要访问在业务逻辑内部创建的对象(除非通过某种工厂模式注入或提供),这意味着修改主要源代码。同时,在一个地方模拟 API 有一个缺点:它是一种通用解决方案,可能会实现成功执行。对于测试错误情况,可能需要在测试本身的实例中进行模拟,但是在您的业务逻辑中,您可能无法直接访问例如post内心深处创造的。

那么,让我们看一下模拟成功 API 调用的一般情况:

测试/模拟.js

var mongoose = require("mongoose");

// this method is propagated from node-mongodb-native
mongoose.Collection.prototype.insert = function(docs, options, callback) {
    // this is what the API would do if the save succeeds!
    callback(null, docs);
};

module.exports = mongoose;

一般只要创建好模型after修改猫鼬,可以想象上述模拟是在每个测试的基础上完成的,以模拟任何行为。但是,在每次测试之前,请确保恢复到原始行为!

最后,这就是我们对所有可能的数据保存操作的测试的样子。注意,这些并不是我们特有的Post模型,并且可以对所有其他模型进行完全相同的模拟。

测试/test_model.js

// now we have mongoose with the mocked API
// but it is essential that our models are created AFTER 
// the API was mocked, not in the main source!
var mongoose = require("./mock"),
    assert = require("assert");

var underTest = require("../src");

describe("Post", function() {
    var Post;

    beforeEach(function(done) {
        var conn = mongoose.createConnection();
        Post = underTest.model(conn, underTest.PostSchema, "Post");
        done();
    });

    it("given valid data post.save returns saved document", function(done) {
        var post = new Post({
            title: 'My test post',
            postDate: Date.now()
        });
        post.save(function(err, doc) {
            assert.deepEqual(doc, post);
            done(err);
        });
    });

    it("given valid data Post.create returns saved documents", function(done) {
        var post = new Post({
            title: 'My test post',
            postDate: 876543
        });
        var posts = [ post ];
        Post.create(posts, function(err, docs) {
            try {
                assert.equal(1, docs.length);
                var doc = docs[0];
                assert.equal(post.title, doc.title);
                assert.equal(post.date, doc.date);
                assert.ok(doc._id);
                assert.ok(doc.createdAt);
                assert.ok(doc.updatedAt);
            } catch (ex) {
                err = ex;
            }
            done(err);
        });
    });

    it("Post.create filters out invalid data", function(done) {
        var post = new Post({
            foo: 'Some foo string',
            postDate: 876543
        });
        var posts = [ post ];
        Post.create(posts, function(err, docs) {
            try {
                assert.equal(1, docs.length);
                var doc = docs[0];
                assert.equal(undefined, doc.title);
                assert.equal(undefined, doc.foo);
                assert.equal(post.date, doc.date);
                assert.ok(doc._id);
                assert.ok(doc.createdAt);
                assert.ok(doc.updatedAt);
            } catch (ex) {
                err = ex;
            }
            done(err);
        });
    });

});

值得注意的是,我们仍在测试非常低级别的功能,但我们可以使用相同的方法来测试使用的任何业务逻辑Post.create or post.save内部。

最后一点,让我们运行测试:

〜/source/web/xxx $ npm 测试

> [email protected] /cdn-cgi/l/email-protection test /Users/osklyar/source/web/xxx
> mocha --recursive

Post
  ✓ given valid data post.save returns saved document
  ✓ given valid data Post.create returns saved documents
  ✓ Post.create filters out invalid data

3 passing (52ms)

我必须说,这样做一点也不有趣。但通过这种方式,它实际上是业务逻辑的纯粹单元测试,没有任何内存中或真实的数据库,并且相当通用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Mocking/Stubbing Mongoose 模型保存方法 的相关文章

  • 如何设置黄瓜环境变量

    我有以下 package json name newcucumber version 1 0 0 main index js scripts test node modules bin cucumber js firefox node mo
  • Express.js - 监听关闭

    我有一个使用 Express 的 Node js 应用程序 在该应用程序中 我有一个如下所示的块 const app require app const port process env PORT 8080 const server app
  • Node.js 循环发送 http 请求

    我实际上遇到了使用 node js 执行的 javascript 代码的问题 我需要循环发送http请求到远程服务器 我在代码中设置了www google ca 这是我的代码 var http require http var option
  • Puppeteer 无法在 VPS (DigitalOcean) 上工作

    我在水滴中数字海洋 https www digitalocean com 我收到这个错误 node 5549 UnhandledPromiseRejectionWarning TimeoutError Navigation Timeout
  • mongoDB白名单IP

    我看到类似的帖子 但没有一个能帮助我解决我的问题 在学习了从头开始构建 MERN 应用程序的 Udemy 教程后 我陷入了 mongoose 连接的困境 这是我的 index js 代码 const express require expr
  • 节点项目的 Azure git 部署失败

    我正在尝试将我的项目部署到azure 它正在失败 这些是我采取的步骤 git init git config core longpaths true git add git commit m initial commit 所有这些都有效 我
  • 使用 Laravel dusk 仅迁移一次

    根据到 数据库测试 文档 https laravel com docs 5 4 database testing resetting the database after each test我可以在每次测试后重置数据库 第一个选项 第二个选
  • 嵌入文档中的mongodb限制

    我需要创建一个消息系统 一个人可以在其中与许多用户进行对话 例如 我开始与 user2 user3 和 user4 交谈 因此他们中的任何人都可以看到整个对话 并且如果对话在任何时候都不是私密的 则任何参与者都可以将任何其他人添加到对话中
  • 在使用 supertest 和 Node.js 的测试中,res.body 为空

    我正在测试 Node js API超级测试 https github com visionmedia supertest 我无法解释为什么res body对象超集返回为空 数据显示在res text对象 但不是res body 知道如何解决
  • Google 钱包 API Codelabs 示例权限被拒绝

    我正在研究使用 Google 钱包通过 Web jwt 界面存储条形码 用于电子处方 如果重要的话 的可行性 https codelabs developers google com add to wallet web 0 https co
  • 如何从 NodeJs 调用 python 脚本

    我需要在 NodeJs 中调用这个 python 脚本 Read py usr bin env python coding utf8 import RPi GPIO as GPIO import MFRC522 import signal
  • 用嘲笑测试 Laravel 外观总是会通过,即使它应该失败

    我试图在单元测试期间模拟 Laravel 中的一些外观 但似乎无论如何测试总是会通过 例如 此示例取自 Laravel 文档 Event shouldReceive fire gt once gt with foo array name g
  • 如何使用express在node.js中使浏览器关闭后会话过期?

    我在用 req session cookie maxAge 14 24 3600 1000 0 让会话在两周后过期 但现在我想让它在浏览器关闭后过期 有什么办法可以做到吗 来自连接会话中间件文档 https github com expre
  • 元素数组中数组的 MongoDB 全文

    当元素数组中的数组包含应与我的搜索匹配的文本时 我无法检索文档 以下是两个示例文档 id foo name Thing1 data text X X name Thing2 data text X Y id foo
  • isModified 并预保存 mongoose...Nodejs

    您好 我只想在密码更改时使用散列密码保存 因此我在预保存中使用了 isModified 函数 但即使我更改了密码 它也总是返回 false 我尝试这样做的原因是因为我不想在更改其他属性时更改并保存我的密码 router post chang
  • 如何在 Angular 单元测试中模拟/触发 $routeChangeSuccess?

    给定一个附加到 routeChangeSuccess 事件的处理程序来更新 rootScope 上的 title 属性 rootScope on routeChangeSuccess function event current previ
  • Mongo按动态字段排序

    所以我传入了一个动态变量 它是我想要排序的字段的名称 假设下面的 sortVariable 可能等于 price createdAt name 等 这不起作用 我该怎么做 function findStuff sortVariable va
  • 使用 NVM 安装多个版本的 node.js (Ubuntu)

    如何使用 Ubuntu 在 Ubuntu 中安装多个版本的 Node jsNVM https github com creationix nvm 先验知识 如何使用终端 例如 您可以使用gnome terminal 安装依赖项 sudo a
  • Socket.io 与服务器离线连接

    如何检测服务器是否离线或由于其他原因无法连接 我的代码看起来像这样 this socket io connect connectionInfo reconnect false 它不会抛出任何错误 因此 try catch 子句不起作用 Us
  • 永远运行 Gulp 脚本?

    是否可以运行 Gulp 脚本forever https www npmjs com package forever 我有 Gulp 脚本想要作为守护进程运行 所以我可以启动 停止 列出它 好的 所以我通过将 gulp 二进制文件从 usr

随机推荐