29 Jun 2016 · Software Engineering

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

    13 min read
    Contents

    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] (https://semaphoreci.com/community/tutorials/npm-node-js-package-manager). 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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    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.