神刀安全网

Test Driven RESTful API development using Node and MongoDB

Introduction

In recent days, I am trying to push myself to write test cases for all the Javascript codes in my project. Because Test Driven Development will help to make your code more stable, less error prone, loosely coupled from other module and the code will not break very easily.

This article will cover how to develop a RESTful Todo API in Test Driven method.

Technology Stack

We’ll be using Node as server and MongoDB with Mongoose ODM as the database. For test runner, we’ll use Mocha and some testing libraries like Sinon , mongoose-sinon and should JS . Last but not least Grunt for task running.

Project Setup

Now it’s the time to get our hands dirty. Before start developing the actual API, we have to setup the folder and end point for our app. For folder setup, I’ll leave it to your choice or you can check my GitHub repo.

Next, let’s decide our endpoint.

Action HTTP Routes
Get all todo GET /api/todos
Post a todo POST /api/todos
Update a todo PUT /api/todos/:id
Delete a todo DELETE /api/todos/:id

Installing dependencies

Now we’ll install our project specific dependencies using NPM . First, we’ll do a npm init to initiate a node project and then installing our dependencies.

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

The packages will be saved in our package.json for our future use.

Database setup

Now we will starting our MongoDB local instance. If you want to setup MongoDB, I’ll prefer MongoDB official document for your reference.

Start the local MongoDB server using the below command.

> mongod

Now MongoDB will run in default 27017 port.

Defining schema for our Todo

Now we’ll be setting up the schema for our database. Mongoose has a straight forward method mongoose.Schema to create schema for our todo.

// app/models/todo.model.js var mongoose = require('mongoose'); var Schema = mongoose.Schema;  // TODO schema var TodoSchema = new Schema({ todo: String, completed: { type:Boolean, default: false }, created_by: { type: Date, default: Date.now } });

Starting our express server

The next step would be making our server to run. So just require all the dependencies into our server.js

//server.js var express = require('express'); var mongoose = require('mongoose'); var morgan = require('morgan'); var bodyParser = require('body-parser'); var methodOverride = require('method-override'); var config = require('./app/config/config'); var cors = require('cors'); var app = express();  app.use(morgan('dev')); // log every request to the console app.use(cors()); 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());  app.listen(config.port); console.log("App listening on port "+config.port);

Let’s start our server by node server.js in the terminal. Make sure you get the log message in your console.

App listening on port 2000

Connecting to Database

We are done with setting up the server, Now it’s the time to connect our database. Mongoose gives a handy method to connect our database with the express application through mongoose.connect.

// app/config/config.js var config = { port: process.env.PORT || 2000, db: process.env.MONGOLAB_URI || "mongodb://localhost/todoapi" } module.exports = config;  // server.js var db = mongoose.connect(config.db); mongoose.connection.on('connected', function () { console.log('Mongoose default connection open to ' + config.db); });

Now we’ll be getting a log in our console like below

Mongoose default connection open to mongodb://localhost/todoapi

Writing The Application Logic

Let’s build our actual Todo API. We’ll start of with configuring router for our end point.

Implementing the router

// app/routers/route.js  var Todo = require('../models/todo.model'); var TodoController = require('../controllers/todo.controller')(Todo);  module.exports = function(app){ app.get('/api/todos', TodoController.GetTodo); app.post('/api/todos', TodoController.PostTodo); app.put('/api/todos/:todo_id', TodoController.UpdateTodo); app.delete('/api/todos/:todo_id', TodoController.DeleteTodo); }

For each router endpoint, We’ll have a controller action which will be pointed in the TodoController .

Controllers for our end point

// app/controller/todo.controller.js  var TodoCtrl = function(Todo){ var TodoObj = {}; TodoObj.PostTodo = function(req, res, next){ var newTodo = new Todo(req.body); newTodo.save(function(err, todo){ if(err){ res.json({status: false, error: err.message}); return; } res.json({status: true, todo: todo}); }); }  TodoObj.GetTodo = function(req, res, next){ Todo.find(function(err, todos){ if(err) { res.json({status: false, error: "Something went wrong"}); return } res.json({status: true, todo: todos}); }); }  TodoObj.UpdateTodo = function(req, res, next){ var completed = req.body.completed; Todo.findById(req.params.todo_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"}); }); }); }  TodoObj.DeleteTodo = function(req, res, next){ Todo.remove({_id : req.params.todo_id }, function(err, todos){ if(err) { res.json({status: false, error: "Deleting todo is not successfull"}); } res.json({status: true, message: "Todo deleted successfully"}); }); }  return TodoObj; } module.exports = TodoCtrl;

Now our API is ready and I’ve hosted it in Heroku . If you need more information on setup procedure Heroku for Node , Check this document for your reference.Now our Todo API is is ready and we can build Todo application with any front end framework.

Writing test case for API

Now we’ll be writing test case for our API using Sinon which is a standalone mocking library andShould JS which is an assertion library. To get a detail information on Sinon and Should JS refer the documentation.

First, we’ll start with unit test cases and then integration test.

Test cases for Todo Controller

// test/app/controller/todo.controller.test.js "use strict";  var should = require('should'), sinon = require('sinon'), mongoose = require('mongoose');  require('sinon-mongoose');  var TodoModel = require('../../../app/models/todo.model');  describe('TodoController testing', function () {  describe('Todo Post test', function () {  it('Should call save only once', function () { var saveStub = sinon.stub(); function Book(){ this.save = saveStub } var req = { body: { todo: "Test todo from mock" } } var res = {}, next = {}; var TodoController = require('../../../app/controllers/todo.controller')(Book); TodoController.PostTodo(req, res, next); sinon.assert.calledOnce(saveStub); });  it('Should save todo', function (done) { var todoMock = sinon.mock(new TodoModel({ todo: 'Save new todo from mock'})); var todo = todoMock.object;  todoMock .expects('save') .yields(null, 'SAVED');  todo.save(function(err, result) { todoMock.verify(); todoMock.restore(); should.equal('SAVED', result, "Test fails due to unexpected result") done(); }); });  });  describe('Get all Todo test', function () { it('Should call find once', function (done) { var TodoMock = sinon.mock(TodoModel); TodoMock .expects('find') .yields(null, 'TODOS');  TodoModel.find(function (err, result) { TodoMock.verify(); TodoMock.restore(); should.equal('TODOS', result, "Test fails due to unexpected result") done(); }); }); });  describe('Delete todo test', function () { it('Should delete todo of gived id', function (done) { var TodoMock = sinon.mock(TodoModel);  TodoMock .expects('remove') .withArgs({_id: 12345}) .yields(null, 'DELETED');  TodoModel.remove({_id: 12345}, function(err, result){ TodoMock.verify(); TodoMock.restore(); done(); })  }); });  describe('Update a todo', function () { it('Should update the todo with new value', function (done) { var todoMock = sinon.mock(new TodoModel({ todo: 'Save new todo from mock'})); var todo = todoMock.object;  todoMock .expects('save') .withArgs({_id: 12345}) .yields(null, 'UPDATED');  todo.save({_id: 12345}, function(err, result){ todoMock.verify(); todoMock.restore(); done(); })  }); });  });

Now we’re done with our unit test cases. Use npm test to get the output in your terminal

Test Driven RESTful API development using Node and MongoDB

Integration test for Todo API

Next testing would be integration testing. We’ve to connect the test database and check the actual Todo functionality.

SO we’ll be using grunt-env for configuring enviromental variable for our application.

// Gruntfile.js  .... env: { options: {  }, test: { NODE_ENV: 'test' }, prod: { NODE_ENV: 'production' } } ....  grunt.loadNpmTasks('grunt-env'); grunt.registerTask('integrationTest', ['env:test', 'simplemocha:integrationTest']);  // server.js  var db;  if(process.env.NODE_ENV === "test"){ db = mongoose.connect(config.test_db); app.listen(config.test_port); console.log("App listening on port "+config.test_port); }else{ db = mongoose.connect(config.db); app.listen(config.port); console.log("App listening on port "+config.port); }  mongoose.connection.on('connected', function () { console.log('Mongoose default connection open to ' + config.db); });  // app/config/config.js  var config = { port: process.env.PORT || 2000, db: process.env.MONGOLAB_URI || "mongodb://localhost/todoapi", test_port: 2001, test_db: "mongodb://localhost/todoapi_test" }

Now in test environment, the database would connect to todoapi_test . So let’s start writing our integration test.

For HTTP assertion, we’ll be using supertest library.

"use strict"; var should = require('should'), request = require('supertest'), app = require('../../server.js'), mongoose = require('mongoose'), Todo = mongoose.model('Todo'), agent = request.agent(app);  describe('Todo CRUD integration testing', function () {  describe('Get all todo', function () {  before(function (done) { var newTodo = { todo: "Todo from hooks" }; agent .post('/api/todos') .end(function(){ done(); }) });  it('Should get status equal success and array of todo', function (done) { agent .get('/api/todos') .expect(200) .end(function(err, results){ results.body.status.should.equal(true); done(); }); });  });  describe('Post a todo', function () { it('Should allow post to post a todo and return _id', function (done) { var params = { todo: "Todo fro testing" }; agent .post('/api/todos') .send(params) .expect(200) .end(function(err, results){ results.body.todo.completed.should.equal(false); results.body.todo.should.have.property('_id'); done(); }); }); });  describe('Delete a todo', function () { var id; before(function (done) { var params = { todo: "Todo from hooks to delete" }; agent .post('/api/todos') .send(params) .end(function(err, result){ id = result.body.todo._id; done(); }) });  it('Should delete the todo by _id', function (done) { agent .delete('/api/todos/'+id) .end(function(err, result){ result.body.status.should.equal(true); done(); })  });  });  describe('Update a todo', function () { var id; before(function (done) { var newTodo = { todo: "Todo from hooks to update" }; agent .post('/api/todos') .send(newTodo) .end(function(err, result){ id = result.body.todo._id; done(); }) });  it('Should update the completed status of todo by _id to true', function (done) { var params = { completed: true }; agent .put('/api/todos/'+id) .send(params) .end(function(err, result){ result.body.status.should.equal(true); done(); })  }); });  });

We’re done with our unit test cases. Use grunt integrationTest to get the output in your terminal

Test Driven RESTful API development using Node and MongoDB

Todo

  • Integrating with Travis and Codecov for measuring the code coverage

Conclusion

We now have a basic RESTfulTodo API built with Node and MongoDB with unit and integration test done. Be sure to comment below if you have any questions.

Grab the code from the GitHub and working Demo

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Test Driven RESTful API development using Node and MongoDB

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮