web-dev-qa-db-ja.com

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

ただし、これを使用すると、テストを実行するたびにデータベースにアクセスすることになり、避けたいと思います。

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呼び出しをスタブ化しようとしました。

_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メソッドが返すものと比較したいと思います。これは、データベースにアクセスした最初のテストと同じです。私は couplemethods を試しましたが、すべてハックだと感じています。きれいな方法があるに違いない?

いくつかの質問:

  • 私はいつもどこでも読んだことがあるように、データベースにアクセスすることを本当に避けるべきですか?最初の例は問題なく動作し、実行するたびにデータベースをクリアできました。しかし、それは本当に私には正しくないと思います。

  • Mongooseモデルのsaveメソッドをどのようにスタブ化して、テストしたいものを実際にテストすることを確認します。つまり、新しいオブジェクトをdbに保存します。

15
cabaret

基本

単体テストでは、DBにアクセスしないでください。私は1つの例外を考えることができます:インメモリDBにアクセスしますが、複雑なプロセス(したがって、実際には機能の単位ではない)のためにメモリに保存された状態のみが必要であるため、統合テストの領域にすでにあります。つまり、実際のDBはありません。

単体テストでテストしたいのは、ビジネスロジックによって、アプリケーションとDBの間のインターフェースで正しいAPI呼び出しが行われることです。 DB API /ドライバーの開発者がAPIの下のすべてが期待どおりに動作することを適切にテストしたと想定できます。ただし、成功した保存、データの一貫性による失敗、接続の問題による失敗など、ビジネスロジックがさまざまな有効なAPI結果にどのように反応するかをテストでカバーする必要もあります。

これは、あなたが必要とし、モックしたいのは、DBドライバーインターフェースの下にあるすべてであることを意味します。ただし、その動作をモデル化して、DB呼び出しのすべての結果についてビジネスロジックをテストできるようにする必要があります。

これは、使用するテクノロジーを介してAPIにアクセスする必要があり、APIを知る必要があることを意味するため、言うよりも簡単です。

マングースの現実

Mongooseが使用する基本的な「ドライバー」によって実行される呼び出しをモックしたい基本に固執します。それが node-mongodb-native であると仮定すると、これらの呼び出しをモックアウトする必要があります。 mongooseとネイティブドライバ間の完全な相互作用を理解することは簡単ではありませんが、後者はmongoose.Collectionしないのようなメソッドを再実装するため、一般にmongoldb.Collectionのメソッドに帰着します。 insert。この特定のケースでinsertの動作を制御できる場合は、APIレベルでDBアクセスを偽造したことがわかります。両方のプロジェクトのソースでそれを追跡できます。つまり、Collection.insertは実際にはネイティブドライバーメソッドです。

あなたの特定の例では、完全なパッケージで パブリックGitリポジトリ を作成しましたが、ここではすべての要素を回答に投稿します。

ソリューション

個人的には、mongooseを操作する「推奨」方法はまったく使えないことに気づきます。モデルは通常、対応するスキーマが定義されているモジュールで作成されますが、すでに接続が必要です。同じプロジェクト内の完全に異なるmongodbデータベースと通信するために複数の接続を使用する目的、およびテスト目的では、これは人生を本当に困難にします。実際、懸念が完全に切り離されるとすぐに、少なくとも私にとってはマングースはほとんど使用できなくなります。

したがって、最初に作成するのは、パッケージ記述ファイル、スキーマと一般的な「モデルジェネレーター」を備えたモジュールです。

package.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を1か所でモックすることには欠点があります。それは、おそらく成功した実行を実装する一般的なソリューションです。エラーケースのテストでは、テスト自体のインスタンスのモックが必要になる場合がありますが、その場合、ビジネスロジック内では、次のインスタンスに直接アクセスできない可能性があります。 postが内部に作成されました。

それでは、成功したAPI呼び出しを模擬する一般的なケースを見てみましょう。

test/mock.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 mongooseを変更する場合、上記のモックはすべての動作をシミュレートするためにテストごとに実行されると考えられます。ただし、すべてのテストの前に、必ず元の動作に戻してください。

最後に、これはすべての可能なデータ保存操作のテストがどのように見えるかです。これらはPostモデルに固有のものではなく、まったく同じモックを備えた他のすべてのモデルで実行できることに注意してください。

test/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またはpost.saveを内部で使用するビジネスロジックをテストする場合も、この同じアプローチを使用できます。

最後のビット、テストを実行しましょう:

〜/ source/web/xxx $ npm test

> [email protected] 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)

私は言わなければならない、これはそのようにそれを行うのは楽しいことではありません。しかし、この方法は、インメモリDBや実際のDBがなく、かなり一般的なビジネスロジックの純粋な単体テストです。

40
Oleg Sklyar

テストが必要な場合はstatic'sおよびmethod's特定のマングースモデルの場合、 sinon および sinon-mongoose を使用することをお勧めします。 (それは chai と互換性があると思います)

これにより、Mongo DBに接続する必要がなくなります。

あなたの例に続いて、静的メソッドfindLastがあると仮定します

//If you are using callbacks
PostSchema.static('findLast', function (n, callback) {
  this.find().limit(n).sort('-postDate').exec(callback);
});

//If you are using Promises
PostSchema.static('findLast', function (n) {
  this.find().limit(n).sort('-postDate').exec();
});

次に、このメソッドをテストするには

var Post = mongoose.model('Post');
// If you are using callbacks, use yields so your callback will be called
sinon.mock(Post)
  .expects('find')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-postDate')
  .chain('exec')
  .yields(null, 'SUCCESS!');

Post.findLast(10, function (err, res) {
  assert(res, 'SUCCESS!');
});

// If you are using Promises, use 'resolves' (using sinon-as-promised npm) 
sinon.mock(Post)
  .expects('find')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-postDate')
  .chain('exec')
  .resolves('SUCCESS!');

Post.findLast(10).then(function (res) {
  assert(res, 'SUCCESS!');
});

sinon-mongoose リポジトリで実用的な(そして簡単な)例を見つけることができます。

7
Gon