A tdd approach to building a restful api using node.js and mongodb

A TDD Approach to Building a Todo API Using Node.js and MongoDB

Learn how to develop a Todo API with Node.js, MongoDB, Mocha and Sinon.js, using the test-driven development approach.

Brought to you by

Semaphore

Note that this tutorial is no longer considered valid as it does not match our quality standards.

Introduction

Testing is an integral part of the software development process which helps improve the quality of the software. There are many types of testing involved like manual testing, integration testing, functional testing, load testing, unit testing, and other. In this article, we'll write our code following the rules of Test Driven Development (TDD).

What is a unit test?

Martin Fowler defines unit tests as follows:

Firstly, there is a notion that unit tests are low-level, focusing on a small part of the software system. Secondly, unit tests are usually written these days by the programmers themselves using their regular tools - the only difference being the use of some sort of unit testing framework. Thirdly, unit tests are expected to be significantly faster than other kinds of tests.

In this tutorial, we'll be building a Todo API using the TDD method with Node.js and MongoDB. We'll write unit tests for the production code first, and the actual production code later.

Prerequisites

  • Express.js,
  • MongoDB,
  • Mocha,
  • Chai, and
  • Sinon.js.

Project Setup

Before we start developing our actual API, we have to set up the folder and end point.

In a software project, there is no perfect way to structure an application. Take a look at this GitHub repository for the folder structure followed in this tutorial.

Now, let’s create our endpoints:

table

Installing Dependencies

Node.js has its own package management called NPM. To learn more about NPM, you can read our Node.js Package Manager tutorial. Now, let’s go ahead and install our project dependencies.

npm install express mongoose method-override morgan body-parser cors save-dev

Defining Schema

We’ll be using Mongoose as an Object Document Model for Node.js that works like a typical ORM, the same way ActiveRecord works for Rails. Mongoose helps access MongoDB commands easily. Let’s start defining our schema for our Todo API.

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
// Defining schema for our Todo API
var TodoSchema = Schema({
  todo: {
    type: String
  },
  completed: {
    type: Boolean,
    default: false
  },
  created_by: {
    type: Date,
    default: Date.now
  }
});
//Exporting our model
var TodoModel = mongoose.model('Todo', TodoSchema);

module.exports = TodoModel;

Everything in Mongoose starts with a schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

In the above todo schema, we've created three fields which will store the todo description, status of the todo, and date created. This schema helps our Node.js application understand how to map data from the MongoDB into JavaScript objects.

Setting Up the Express Server

For setting up our server, we’ll be using Express which is a minimal Node.js web framework which provides a robust set of features for developing a web application.

Let’s go ahead and set up our Express server.

First, we'll import our project dependencies as follows:

var express = require('express');
var mongoose = require('mongoose');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var app = express();
var config = require('./app/config/config');

Next, we'll configure the Express middleware as follows:

app.use(morgan('dev'));                                         // log every request to the console
app.use(bodyParser.urlencoded({'extended':'true'}));            // parse application/x-www-form-urlencoded
app.use(bodyParser.json());                                     // parse application/json
app.use(bodyParser.json({ type: 'application/vnd.api+json' })); // parse application/vnd.api+json as json
app.use(methodOverride());

Managing the Mongoose Connection

To connect MongoDB with your application, call mongoose.connect, which will set up a connection with the database. This is the minimum needed to connect our todoapi database running locally on the default port 27017. If the local connection fails, try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed.

//Connecting MongoDB using mongoose to our application
mongoose.connect(config.db);

//This callback will be triggered once the connection is successfully established to MongoDB
mongoose.connection.on('connected', function () {
  console.log('Mongoose default connection open to ' + config.db);
});

//Express application will listen to port mentioned in our configuration
app.listen(config.port, function(err){
  if(err) throw err;
  console.log("App listening on port "+config.port);
});

Start the server using the command below.

//starting our node server
> node server.js
App listening on port 2000

Writing our test cases for our API

In TDD, we write the test cases for our application by taking all of the possible input, output and errors into account. Let's write the test cases for our Todo API.

Setting Up the Test Environment

As mentioned earlier in the tutorial, we'll be using Mocha as a test runner, Chai as an assertion library, and Sinon.js for mocking the Todo Models. First, let's install our dependencies for our unit testing:

> npm install mocha chai sinon sinon-mongoose --save

We'll be using sinon-mongoose module for mocking our MongoDB model defined using Mongoose.

Now, we'll import the test dependencies as follows:

var sinon = require('sinon');
var chai = require('chai');
var expect = chai.expect;

var mongoose = require('mongoose');
require('sinon-mongoose');

//Importing our todo model for our unit testing.
var Todo = require('../../app/models/todo.model');

Test Cases for the Todo API

When writing unit tests, we need to consider both success and error scenarios. For our Todo API, we'll write test cases for both success and error scenarios for creating, deleting, updating and getting todo through our API. We're going to write unit tests for our Todo API using Mocha, Chai and Sinon.js.

Get all Todo

In this section, we're going to write test cases for getting all saved todos from our database. We need to write test cases for both success and error scenarios to ensure that our code will work properly in both cases in production.

We're not going to do unit tests using the real database, so we'll be using sinon.mock to create a mock model for our Todo schema and we'll test the expected result.

Let's create a mock for our Todo model using sinon.mock and test our API for getting all todos saved in the database using mongoose find method.

    describe("Get all todos", function(){
         // Test will pass if we get all todos
        it("should return all todos", function(done){
            var TodoMock = sinon.mock(Todo);
            var expectedResult = {status: true, todo: []};
            TodoMock.expects('find').yields(null, expectedResult);
            Todo.find(function (err, result) {
                TodoMock.verify();
                TodoMock.restore();
                expect(result.status).to.be.true;
                done();
            });
        });

        // Test will pass if we fail to get a todo
        it("should return error", function(done){
            var TodoMock = sinon.mock(Todo);
            var expectedResult = {status: false, error: "Something went wrong"};
            TodoMock.expects('find').yields(expectedResult, null);
            Todo.find(function (err, result) {
                TodoMock.verify();
                TodoMock.restore();
                expect(err.status).to.not.be.true;
                done();
            });
        });
    });

Save a New Todo

For saving a new todo, we need to mock the Todo model with a sample task. We'll check the result using the mock Todo model we've created for saving the todo in the database using the mongoose save method.

    // Test will pass if the todo is saved
    describe("Post a new todo", function(){
        it("should create new post", function(done){
            var TodoMock = sinon.mock(new Todo({ todo: 'Save new todo from mock'}));
            var todo = TodoMock.object;
            var expectedResult = { status: true };
            TodoMock.expects('save').yields(null, expectedResult);
            todo.save(function (err, result) {
                TodoMock.verify();
                TodoMock.restore();
                expect(result.status).to.be.true;
                done();
            });
        });
        // Test will pass if the todo is not saved
        it("should return error, if post not saved", function(done){
            var TodoMock = sinon.mock(new Todo({ todo: 'Save new todo from mock'}));
            var todo = TodoMock.object;
            var expectedResult = { status: false };
            TodoMock.expects('save').yields(expectedResult, null);
            todo.save(function (err, result) {
                TodoMock.verify();
                TodoMock.restore();
                expect(err.status).to.not.be.true;
                done();
            });
        });
    });

Update a Todo Based on Its ID

In this section, we're going to check the update function our API. This is going to be similar to the example above, except we'll be mocking our Todo model with an ID as an argument using withArgs method.

  // Test will pass if the todo is updated based on an ID
  describe("Update a new todo by id", function(){
    it("should updated a todo by id", function(done){
      var TodoMock = sinon.mock(new Todo({ completed: true}));
      var todo = TodoMock.object;
      var expectedResult = { status: true };
      TodoMock.expects('save').withArgs({_id: 12345}).yields(null, expectedResult);
      todo.save(function (err, result) {
        TodoMock.verify();
        TodoMock.restore();
        expect(result.status).to.be.true;
        done();
      });
    });
    // Test will pass if the todo is not updated based on an ID
    it("should return error if update action is failed", function(done){
      var TodoMock = sinon.mock(new Todo({ completed: true}));
      var todo = TodoMock.object;
      var expectedResult = { status: false };
      TodoMock.expects('save').withArgs({_id: 12345}).yields(expectedResult, null);
      todo.save(function (err, result) {
        TodoMock.verify();
        TodoMock.restore();
        expect(err.status).to.not.be.true;
        done();
      });
    });
  });

Delete a Todo Based on Its ID

This is going to be the last section of our unit tests for the Todo API. In this section, we'll be testing the delete functionality of our API based on the given ID using the mongoose remove method.

    // Test will pass if the todo is deleted based on an ID
    describe("Delete a todo by id", function(){
        it("should delete a todo by id", function(done){
            var TodoMock = sinon.mock(Todo);
            var expectedResult = { status: true };
            TodoMock.expects('remove').withArgs({_id: 12345}).yields(null, expectedResult);
            Todo.remove({_id: 12345}, function (err, result) {
                TodoMock.verify();
                TodoMock.restore();
                expect(result.status).to.be.true;
                done();
            });
        });
        // Test will pass if the todo is not deleted based on an ID
        it("should return error if delete action is failed", function(done){
            var TodoMock = sinon.mock(Todo);
            var expectedResult = { status: false };
            TodoMock.expects('remove').withArgs({_id: 12345}).yields(expectedResult, null);
            Todo.remove({_id: 12345}, function (err, result) {
                TodoMock.verify();
                TodoMock.restore();
                expect(err.status).to.not.be.true;
                done();
            });
        });
    });

We have to restore our Todomock every time to make sure it works in the next section.

When we run our test cases for the first time all of them should fail, because our production code is not ready yet. We'll run the automated tests until all the unit tests have passed.

> npm test

  Unit test for Todo API
    Get all todo
      1) should return all todo
      2) should return error
    Post a new todo
      3) should create new post
      4) should return error, if post not saved
    Update a new todo by id
      5) should updated a todo by id
      6) should return error if update action is failed
    Delete a todo by id
      7) should delete a todo by id
      8) should return error if delete action is failed

  0 passing (17ms)
  8 failing

Once you run the npm test in the terminal, we'll get the above output in which all of our unit test cases fail. We need to write our application logic based on the requirement and unit test case to make our API more stable.

Writing the Application Logic

The next step is writing the actual application code for our Todo API. We'll run our automated test cases and keep refactoring the code until all of our unit tests pass.

Configuring the Router

For a web application both on the client and the server side, configuring the router is the most important part. In our application, we'll use an instance of the Express Router to handle all of our routes. Let's create the route for our application.

var express = require('express');
var router = express.Router();

var Todo = require('../models/todo.model');
var TodoController = require('../controllers/todo.controller')(Todo);

// Get all Todo
router.get('/todo', TodoController.GetTodo);

// Create new Todo
router.post('/todo', TodoController.PostTodo);

// Delete a todo based on :id
router.delete('/todo/:id', TodoController.DeleteTodo);

// Update a todo based on :id
router.put('/todo/:id', TodoController.UpdateTodo);

module.exports = router;

Controller

Now that we're almost in the final stage of our tutorial, we'll write our controller code. In a typical web application, a controller holds the major application logic for saving data, retrieving data from the database, and validation will be done. Let's write our actual controller for the Todo API, and run the automated unit test cases until all of the tests pass.

    var Todo = require('../models/todo.model');

    var TodoCtrl = {
        // Get all todos from the Database
        GetTodo: function(req, res){
            Todo.find({}, function(err, todos){
              if(err) {
                res.json({status: false, error: "Something went wrong"});
                return;
              }
              res.json({status: true, todo: todos});
            });
        },
        //Post a todo into Database
        PostTodo: function(req, res){
            var todo = new Todo(req.body);
            todo.save(function(err, todo){
              if(err) {
                res.json({status: false, error: "Something went wrong"});
                return;
              }
              res.json({status: true, message: "Todo Saved!!"});
            });
        },
        //Updating a todo status based on an ID
        UpdateTodo: function(req, res){
            var completed = req.body.completed;
            Todo.findById(req.params.id, function(err, todo){
            todo.completed = completed;
            todo.save(function(err, todo){
              if(err) {
                res.json({status: false, error: "Status not updated"});
              }
              res.json({status: true, message: "Status updated successfully"});
            });
            });
        },
        // Deleting a todo baed on an ID
        DeleteTodo: function(req, res){
          Todo.remove({_id: req.params.id}, function(err, todos){
            if(err) {
              res.json({status: false, error: "Deleting todo is not successfull"});
              return;
            }
            res.json({status: true, message: "Todo deleted successfully!!"});
          });
        }
    }

module.exports = TodoCtrl;

Running test cases

We're done with both the unit test cases and the controller logic for the application. Let's run the test, to see the final result.

> npm test
  Unit test for Todo API
    Get all todo
      ✓ should return all todo
      ✓ should return error
    Post a new todo
      ✓ should create new post
      ✓ should return error, if post not saved
    Update a new todo by id
      ✓ should updated a todo by id
      ✓ should return error if update action is failed
    Delete a todo by id
      ✓ should delete a todo by id
      ✓ should return error if delete action is failed


  8 passing (34ms)

The final result shows that all of our test cases have passed. The next step would be to refactor of the API, which involves repeating the same process we covered in the tutorial.

Conclusion

In this tutorial, we've learned how to design an API using the Test Driven Development approach with Node.js and MongoDB. Although TDD introduces additional complexity to the development process, it helps us build a stable application with fewer errors. Even if you don’t practice TDD, you should at least aim to write tests which will cover all of the functionality of your application.

If you have any questions or thoughts, feel free to leave a comment below.

C726f3bfaf9436cc7825fdc696f04993
Raja Sekar

Raja Sekar is a JavaScript developer/technical writer from Chennai, India. He is passionate about building high-performance and scalable web applications. Blogs at rajasekarm.com.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.