necroface
necroface

Reputation: 3465

node.js - Apply sinon on mongodb unit tests

I implemented a model function for mongodb with node-mongodb-native:

'use strict';

const mongo = require('mongodb');

class BlacklistModel {

    constructor(db, tenant_id, logger) {
        this._db = db;
        this._table = 'blacklist_' + tenant_id;
        this._logger = logger;
    }

    create(data) {
        return new Promise((resolve, reject) => {
            const options = {
                unique: true,
                background: true,
                w: 1
            };
            this._db.collection(this._table).ensureIndex({ phone: 1 }, options, (err) => {
                if (err) {
                    this._logger.error(err);
                    reject(err);
                } else {
                    const datetime = Date.parse(new Date());
                    data._id = new mongo.ObjectID().toString();
                    data.createdAt = datetime;
                    data.updatedAt = datetime;
                    this._db.collection(this._table).insertOne(data, (err) => {
                        if (err) {
                            this._logger.error(err);
                            reject(err);
                        } else {
                            resolve(data);
                        }
                    });
                }
            });
        });
    }

}

module.exports = BlacklistModel;

Now I want to write unit tests for it, considering 3 cases:

Bearing those in minds, here are my tests:

'use strict';

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const expect = chai.expect;

const BlacklistModel = require('../../model/blacklist');

const mongo_url = require('../../config/mongodb');
const MongoClient = require('mongodb').MongoClient;

const logger = require('../../config/logger');

const data = {
    name: 'admin'
};

describe('Model: Blacklist', () => {

    let Blacklist;
    let connected = false;
    let test_db;

    const connect = () => new Promise((resolve, reject) => {
        MongoClient.connect(mongo_url, (err, db) => {
            if (err) {
                reject(err);
            } else {
                Blacklist = new BlacklistModel(db, 'test', logger);
                connected = true;
                test_db = db;
                resolve();
            }
        });
    });

    before(() => connect());

    describe('create', () => {
        let id;
        beforeEach(() => connected ?
            null : connect());
        it('Should return an inserted document', () => {
            return Blacklist.create(data).then(
                (result) => {
                    expect(result._id).to.be.a('string');
                    expect(result.name).to.equal(data.name);
                    expect(result.createdAt).to.be.a('number');
                    expect(result.updatedAt).to.be.a('number');
                    id = result._id;
                });
        });
        it('Should fail to insert a blacklist with the same name', () => {
            const promise = Blacklist.create(data).then(
                (result) => {
                    id = result._id;
                    return Blacklist.create(data);
                });
            return expect(promise).to.be.rejected;
        });
        it('Should fail due to lost connection', () => {
            return test_db.close(true).then(() => {
                connected = false;
                return expect(Blacklist.create(data)).to.be.rejected;
            });
        });
        afterEach(() => connected ?
            Blacklist.delete(id) : connect().then(() => Blacklist.delete(id)));
    });

});

I call real functions in tests, which is seemingly awkward and time-consuming in runtime to avoid side-effects in my humble opinion. But currently I have not come up with any other ideas apart from altering a test database. Is there a way to use sinon? I have read several blogs about sinon, spy, stub, and mock, but struggle to understand and distinguish them. How could I apply them on these tests?

Upvotes: 1

Views: 6294

Answers (2)

鄭元傑
鄭元傑

Reputation: 1647

One simple way to do this is stub and return custom object.
By using this way, you could also verify the functionality by examining the args and return value of stub function.
Here is my example

// your class
class TestCase{
   constructor(db){
      this.db = db;
   }
   method1(args1){
      this.db.insertOne(args1)
   }
   method2(args2){
      this.db.f(args2)
   }
}

// test file
const sinon = require('sinon');

const sandbox = sinon.createSandbox();
const stubInsertOne = sandbox.stub();
const stubFindOne = sandbox.stub();
const stubMongo = {
  insertOne: stubInsertOne,
  findOne: stubFindOne
}
describe("TestCase", ()=>{
  beforeEach(()=>{
     // reset the sandbox or the stub result is polluted
     sandbox.reset();
  })

  it("method1 test", ()=> {
      stubInsertOne.resolves("what ever you want to mock return value");
      
      const testCase = new TestCase(stubMongo);
      testCase.method1();
  })
  .....
})

The downside is that you have to manually stub every function call used in mongodb.

Upvotes: 1

Benji Lees
Benji Lees

Reputation: 815

What you have currently written are integration tests which test the interaction between your node server and mongo db database. Although these tests are more time consuming then mocked unit tests they actually provide far more value. Running queries against a stable MongoDB instance is ensures that your queries are running as planned and that your application is responding properly to the results see: How to unit test a method which connects to mongo, without actually connecting to mongo?.

If you wish to test the javascript functions that manipulate the data as opposed to interaction between the server and db. I would suggest that you refactor out this code from the mongodb query logic and unit test it. Alternatively as you are using a class you should be able to overwrite the _db property with a mock db library. Which would just be an object with methods that mimic that of the mongo library you are currently using. Or you could use sinon to stub out those methods and replace them with methods that return a know result see http://sinonjs.org/releases/v1.17.7/stubs/.

Try something like this:

var ensureIndex = { ensureIndex: sinon.stub() }
sinon.stub(db, 'collection').returns(ensureIndex)

var blackList; 

describe('Model: Blacklist', () => {

  beforeEach(() => {
    var blackList = new BlacklistModel(db, id, logger);
  })
  it('test' => { 
    blackList.create(data).then(() => {
      // some test here
      db.collection.calledWithMatch('some match')
    })

  })
})

Upvotes: 4

Related Questions