4

I am new to Mocha, and only a little experience with Node/Express. My DbProvider module works perfectly (mongodb) when I am access it through my Express app. And now I want to test it. I have read the Mocha site and some tutorials I could find. But I have big trouble of finding an real-world example out there that I could follow (any links much appreciated!).

Here is my unsuccessful attempt to write a testfile:

var DbProvider = require('../db').DbProvider;
var assert     = require('assert');
var dbProvider = new DbProvider('localhost', 27017, 'mydb');
var util       = require('util');

console.log(util.inspect(dbProvider));

describe('DbProvider', function(){
  describe('findAllNotes', function(){
    it('should return some notes', function(){
      dbProvider.findAllNotes({}, function (err, result){
        assert(result.length > 0);
      });
    })
  })
})

The output I get is this:

$ mocha
{}

  ✖ 1 of 1 test failed:

  1) DbProvider findAllNotes should return some notes:
     TypeError: Cannot call method 'collection' of undefined
      at DbProvider.doOperation (/Users/frode/Node/json/db.js:46:11)
      at DbProvider.findAllNotes (/Users/frode/Node/json/db.js:56:8)
      at Context.<anonymous> (/Users/frode/Node/json/test/test.js:15:18)
(cutting out the rest)

It seems that I am unsuccessful to create the dbProvider. This works perfectly in my app... How can I make this work? (And perhaps also: Is the way I have set it up in general ok?)

Edit: Here is the db.js file:

// Database related
'use strict';

var MongoClient       = require('mongodb').MongoClient;
var BSON              = require('mongodb').BSONPure;
var ObjectID          = require('mongodb').ObjectID;
var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
var Validator         = require('validator').Validator
var fieldMaxLength    = 1024;
//var util              = require('util');

var DbProvider = function(host, port, database) {
  var dbUrl = "mongodb://"+host+":"+port+"/"+database;
  var self = this;
  MongoClient.connect(dbUrl, function(err, db) {
    self.db = db;
  });
};

// Do some basic validation on the data we get from the client/user
var validateParams = function(params, callback) {
  // Let´ do a quick general sanity check on the length on all fields
  for(var key in params) {
    if(params[key].length > fieldMaxLength) callback(new Error('Field ' + key + ' is too long.'));
  }
  // and the let us check some specific fields better
  if (params._id) {
    if(checkForHexRegExp.test(params._id)) {
      // In case of '_id' we also need to convert it to BSON so that mongodb can use it.
      params._id = new BSON.ObjectID(params._id);
    } else {
      var err = {error: 'Wrong ID format'};
    }
  }
  if(err) callback(err);
}

// Generalized function to operations on the database
// Todo: Generalize even more when user authenication is implemented
DbProvider.prototype.doOperation = function(collection, operation, params, callback) {
  validateParams(params, callback);
  var operationCallback = function(err, result) {
    callback(err, result);
  };
  this.db.collection(collection, function(err, collection) {
    if(operation==='find') {
      collection.find().toArray(operationCallback);
    } else {
      collection[operation](params, operationCallback);
    }
  });
}

DbProvider.prototype.findAllNotes = function(params, callback) {
  this.doOperation('notes', 'find', params, callback);
};

DbProvider.prototype.findNoteById = function(params, callback) {
  this.doOperation('notes', 'findOne', params, callback);
};

DbProvider.prototype.saveNote = function(params, callback) {
  params.created_at = new Date();
  this.doOperation('notes', 'save', params, callback);
};

DbProvider.prototype.deleteNote = function(params, callback) {
  this.doOperation('notes', 'remove', params, callback);
};

DbProvider.prototype.findUser = function(params, callback) {
  this.doOperation('users', 'findOne', params, callback);
};

exports.DbProvider = DbProvider;

SOLUTION:

After Benjamin told me to handle the async nature of mongodb connecting to the database, and inspired by his suggestion on how to adapt the code, I split the constructor function DbProvider into two parts. The first part, the constructor DbProvider now just saves the db-parameters into a variable. The second part, a new function, DbProvider.connect does the actual async connection. See below.

var DbProvider = function(host, port, database) {
  this.dbUrl = "mongodb://"+host+":"+port+"/"+database;
};

DbProvider.prototype.connect = function(callback) {
  var self = this;
  MongoClient.connect(this.dbUrl, function(err, db) {
    self.db = db;
    callback();
  });
};

So I can now make a Mocha test like this (and async tests also need the "Done" included, like you see in the code below):

var assert     = require('assert');
var DbProvider = require('../db').DbProvider;
var dbProvider = new DbProvider('localhost', 27017, 'nki');

describe('DbProvider', function(){
  describe('findAllNotes', function(){
    it('should return some notes', function(done){
      dbProvider.connect(function(){
        dbProvider.findAllNotes({}, function (err, result){
          assert(result.length > 0);
          done();
        });
      });
    })
  })
})

Note that the acutal test ("should return some notes") is nothing to be proud of. What I wanted here was to get set up so I am able to test something. Now that I finally acutally can do that, I need to write good tests (something in the the line of having a test database, clear it, test insert a document, test search for a document, and so on...).

And in my Express app, I used to set up the database like this:

var DbProvider    = require('./db').DbProvider;
// Setup db instance
var dbProvider = new DbProvider(
  process.env.mongo_host       || 'localhost',
  process.env.mongo_port       || 27017,
  process.env.mongo_db         || 'nki'
);

Now I do the same, but in addition, I call the new connect-function:

// Connect to db. I use (for now) 1 connection for the lifetime of this app.
// And I do not use a callback when connecting here (we do in the testing)
dbProvider.connect(function(){});

Benjamin actually pointed out that it may be ok but not the best practice to have the database set up like this in an Express app. But until I figure out what the best practice really is, I will leave this code as it is. Here is a couple of links reagarding the subject I found (but I have still not concluded of how I will solve it myself):
What's the best practice for MongoDB connections on Node.js? and
[node-mongodb-native] MongoDB Best practices for beginner

If you like, you are very welcome to follow/fork/whatever this project on github. My goal is to get it as production ready I can. The link is https://github.com/frodefi/node-mongodb-json-server

Community
  • 1
  • 1
frodefi
  • 79
  • 2
  • 7
  • 1
    Without the code of `../db` I'm only guessing, but it looks like you're not taking into account the async nature of creating a database connection. Please post the implementation of `DbProvider`. – robertklep Mar 23 '13 at 15:21
  • @frodefi From first glance it seems like your 'params' is your 'callback' in findAllNotes. s – Benjamin Gruenbaum Mar 23 '13 at 15:54
  • @BenjaminGruenbaum, in the test I call it like this: dbProvider.findAllNotes({}, function (err, result)... The {} is the params (but empty, I have params on this function only so I could make the doOperation to fit all usages). Anyway, the console.log(util.inspect(dbProvider)); reveals that the dbProvider is not created in the first place (as I understand it)... I use the very same call in my express app to call this, and it workes there (if I copy/pasted correctly, and I did as far as I can see)... – frodefi Mar 23 '13 at 16:05
  • @frodefi Yeah, you're getting it because MongoClient.connect is asynchronous, see my answer :) – Benjamin Gruenbaum Mar 23 '13 at 16:20

2 Answers2

1

MongoClient.connect is asynchronous.

From the docs:

callback (function) – this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the initialized db object or null if an error occured.

That means DbProvider.db isn't set yet in the test which is why you're getting undefined.

In here:

MongoClient.connect(dbUrl, function(err, db) {
    self.db = db;
});

You're telling it "update self.db after the connection happened", which is at least one event loop tick after this one (but may be more). In your mocha code you're executing your .describe and .it methods right after creating your DbProvider instance which means it was not initialized yet.

I suggest that you re-factor DbProvider to return a callback instead of being a constructor function. Maybe something along the lines of:

var getDbProvider = function(host, port, database,callback) {
  var dbUrl = "mongodb://"+host+":"+port+"/"+database;
  MongoClient.connect(dbUrl, function(err, db) {
    self.db = db;
    callback(db);
  });
};

Which also means moving all the DBProvider methods to an object (maybe the callback will return a dbprovider object and not just a db?).

Another bug solved by using Unit Tests :)

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • So all the (database-realated) tests will then be in this callback, right? Callbacks are still not quite incorporated in my genes... – frodefi Mar 23 '13 at 20:05
  • Yes, all the database-related tests will be in this callback. This is the _only_ correct approach to tackle this issue. – Benjamin Gruenbaum Mar 23 '13 at 20:06
  • Would I do this only in the tests, or should I also do something similar in the Express app? I am guessing I should not, since creating a db connection is not something that is happening every time a request is getting processed. – frodefi Mar 23 '13 at 20:44
  • That is something you need to do in your Express app too, you can move your DB connection around but you _must_ wait for it to become available before processing anything with it – Benjamin Gruenbaum Mar 23 '13 at 20:51
  • Hmmm. Does that mean that this tutorial I have been following is wrong? [howtonode.org/express-mongodb](http://howtonode.org/express-mongodb) Midway down there is articleprovider-mongo.js followed by app.js. As far as I can see in app.js, there is no such construction there? Or I am misunderstanding something? – frodefi Mar 23 '13 at 22:27
  • I need to see your node.js code. What _they_ are doing seems _ok_ (but not best practice) since you'd only access the routes at a later point, by that time it'll establish a DB connection. Having the same DB connection for your app seems a little odd to me. Especially if you want it to scale. – Benjamin Gruenbaum Mar 24 '13 at 00:21
  • What is best parctice after you opinion?In this link [http://stackoverflow.com/questions/11799953/whats-the-best-practice-for-mongodb-connections-on-node-js](http://stackoverflow.com/questions/11799953/whats-the-best-practice-for-mongodb-connections-on-node-js) Are you recommending B or C? (I have not enough stackoverflow to move this into a chat...) – frodefi Mar 24 '13 at 14:40
  • Now you do, feel free to drop by and ask questions http://chat.stackoverflow.com/rooms/17/javascript – Benjamin Gruenbaum Mar 24 '13 at 15:02
0

This is what I used: https://github.com/arunoda/mocha-mongo

It has set of testing helpers for mongodb

Arunoda Susiripala
  • 2,516
  • 1
  • 25
  • 30