Test Driven RESTful API development using Node and MongoDB

2016. 3. 25. 18:05JS&FRONT/NodeJs

번역을 해봅니다.

Introudction

최근에 최대한 내 프로젝트 중에 모든 Javascript 코드들을 작성할 때 테스트 케이스를 작성하려고 노력했습니다.
왜냐하면 TDD(Test Driven Development) 보다 안정적인 코드, 에러를 줄여주며,
다른 모듈과 해당 코드로 부터 느슨한 결합(loosely coupled)을 를 만들도록 해주기 때문이죠.
이 기사는테스트 기반 메소드에서 RESTful Todo API 를 어떻게 개발하는지를 보여드리도록 합니다. 

Technology Stack

서버는 Node를 그리고 데이터베이스로써는 Mongoose ODM으로 하는 MongoDB를 사용하겠습니다.
테스트 러너는 Mocha 와 Sino, mongoose-sinon 그리고 Should JS 와 같은 테스팅 라이브러리를 사용할 것입니다.
마지막으로 task running을 위한 Grunt는 하지 않겠습니다. 

Project Setup

자, 우리의 손을 더럽힐 시간이 왔습니다.
실제 API 개발을 하기 앞서서, 우리는 폴더와 우리 앱을 위한 엔드 포인트를 세팅 해야 합니다.
폴더 세팅을 위해, 여러분의 선택으로 남기거나, GitHub 저장소에서 체크 할수 있습니다. 


다음, 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

NPM을 사용하여 우리 프로젝트의 특정 dependencies를 설치 할 것입니다.
우선 노드 프로젝트를 초기화 하기 위해 npm init 를 하겠습니다. 그리고 우리 dependecies를 설치합니다.

npm install
npm init

package.json
{
  "name""todoapi",
  "version""1.0.0",
  "description""Simple api for todo",
  "main""server.js",
  "scripts"{
    "start""grunt nodemon",
    "test""istanbul cover node_modules/mocha/bin/_mocha && codecov"
  },
  "repository"{
    "type""git",
    "url""git+ssh://git@github.com/rajzshkr/todoapi.git"
  },
  "bin"{
    "codecov""./bin/codecov"
  },
  "keywords"[
    "Node",
    "express",
    "todo"
  ],
  "author""Raja Sekar <rajasekar89be@gmail.com> (http://rajasekarm.com)",
  "license""UNLICENSED",
  "bugs"{
    "url""https://github.com/rajzshkr/todoapi/issues"
  },
  "homepage""https://github.com/rajzshkr/todoapi#readme",
  "dependencies"{
    "body-parser""^1.14.2",
    "codecov""^1.0.1",
    "cors""^2.7.1",
    "express""^4.13.3",
    "method-override""^2.3.5",
    "mongoose""^4.3.5",
    "morgan""^1.6.1"
  },
  "devDependencies"{
    "chai""^3.5.0",
    "codecov""^1.0.1",
    "grunt""^0.4.5",
    "grunt-env""^0.4.4",
    "grunt-environmental""^0.1.5",
    "grunt-nodemon""^0.4.1",
    "grunt-simple-mocha""^0.4.1",
    "istanbul""^0.4.2",
    "mocha""^2.4.5",
    "should""^8.2.2",
    "sinon""^1.17.3",
    "sinon-mongoose""^1.2.1",
    "supertest""^1.2.0"
  },
  "directories"{
    "test""test"
  }
}


Database setup

자, MongoDB 로컬 인스턴스를 시작 할 것입니다.
혹 MongoDB를 세팅해야 한다면, MongoDB offical document를 참고하시길 바랍니다.

local MongoDB 서버를 아래 명령어를 이용해서 시작해보도록 합니다.

mongod

27017 기본포트에서 MongoDB가 시작되었습니다.

Defining schema for our Todo

자, 우리 데이터베이스에 스키마를 세팅해봅시다.
Mongoose는 우리 todo를 위한 스키마를 생성하기 위한 mongoose.Schema 간단한 메소드(straight forward method)를 가지고 있습니다. 

app/models/todo.model.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

// TODO schema
var TodoSchema = new Schema({
   todo: String,
   completed: { type:Booleandefaultfalse },
   created_by: { typeDatedefaultDate.now }
});

// True since it is a parallel middleware
TodoSchema.pre('save'function(nextdone) {
   if(!this.todo){
      next(new Error("Todo should not be null"));
   }
   next();
});

var TodoModel = mongoose.model('Todo'TodoSchema);

module.exports = TodoModel;



Starting our express server

다음 단계는 가동되는 서버를 만들어 줘야 할것 입니다.
그래서  그냥 server.js 안에 모든 dependencies가 필요로합니다.

/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());

require('./app/router/router')(app);

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);
}

module.exports = app;

node server.js 라고 터미널에 명령을 쳐서 서버를 기동해봅시다.
아래와 같은 로그 메세지를 콘솔에서 확인 할 수 있습니다.

App listening on port 2000

Connecting to Database

서버 세팅이 완료 되었습니다.
이제는 데이터 베이스에 연결해 볼 시간이네요.
Mongoose 는  mongoose.connect를 통해 express application와 데이터베이스에 연결하는 간단한 메소드를 제공합니다. 

/app/config/config.js

var config = {
   portprocess.env.PORT || 2000,
   dbprocess.env.MONGOLAB_URI || "mongodb://localhost/todoapi",
   test_port2001,
   test_db"mongodb://localhost/todoapi_test"
}
module.exports = config;

/server.js
mongoose.connection.on('connected'function () {
   console.log('Mongoose default connection open to ' + config.db);
});


자 우리는 아래와 같은 콘솔 메세지를 확인할수 있죠.

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

Writing out  test cases first

우리는 TDD를 따르는 중이고, 컨트롤러에 대한 테스트 케이스를 작성해야하고, 실제 컨트롤러 로직을 구현해야 합니다.
이제, 독립형(standalone) mocking 라이브러리 인 Sinon 과 assertion 라이브러리인 Should JS를 사용한 API를 위한 test case를 작성할 것입니다.
Sinon 과 Should JS에 대한 자세한 내용은 역시 각각의 설명서를 확인해주시면 됩니다.

우선 , unit test cases을 가지고 시작하고, 최종으로 통합테스트를 하도록 합시다요.

/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(reqresnext);
         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(errresult) {
            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 (errresult) {
            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({_id12345})
         .yields(null'DELETED');

         TodoModel.remove({_id12345}function(errresult){
            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({_id12345})
         .yields(null'UPDATED');

         todo.save({_id12345}function(errresult){
            todoMock.verify();
            todoMock.restore();
            done();
         })

      });
   });

});

"npm test" 하고 아래와 같이 확인을 해보도록 하세요.



C:\Users\USER\IdeaProjects\todoapi>npm test

> todoapi@1.0.0 test C:\Users\USER\IdeaProjects\todoapi
> istanbul cover node_modules/mocha/bin/_mocha && codecov



  TodoController testing
    Todo Post test
      √ Should call save only once
      √ Should save todo
    Get all Todo test
      √ Should call find once
    Delete todo test
      √ Should delete todo of gived id
    Update a todo
      √ Should update the todo with new value


  5 passing (42ms)


Writing The Application Logic

실제 Todo API를 빌드 해보자.
end point 를 위한 router 설정하는 것으로 시작합시다.

Implementing the router
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);

}

각각의 router endpoint를 위해서, 우리는 TodoController에 지정되어진 컨트롤러 액션을 가지고 있습니다.

Controller for our end point
"use strict";

var TodoCtrl function(Todo){
   
   var TodoObj = {};

   TodoObj.PostTodo function(reqresnext){
      var newTodo = new Todo(req.body);
      newTodo.save(function(errtodo){
         if(err){
            res.json({statusfalseerror: err.message});
            return;
         }
         res.json({statustruetodo: todo});
      });
   }

   TodoObj.GetTodo function(reqresnext){
      Todo.find(function(errtodos){
         if(err) { 
            res.json({statusfalseerror"Something went wrong"});
            return
         }
         res.json({statustruetodo: todos});
      });
   }

   TodoObj.UpdateTodo function(reqresnext){
      var completed = req.body.completed;
      Todo.findById(req.params.todo_idfunction(errtodo){
         todo.completed = completed;
         todo.save(function(errtodo){
            if(err) { 
               res.json({statusfalseerror"Status not updated"});
            }
            res.json({statustruemessage"Status updated successfully"});
         });
      });
   }

   TodoObj.DeleteTodo function(reqresnext){
      Todo.remove({_id : req.params.todo_id }function(errtodos){
         if(err) { 
            res.json({statusfalseerror"Deleting todo is not successfull"});
         }
         res.json({statustruemessage"Todo deleted successfully"});
      });
   }

   return TodoObj;
}

module.exports TodoCtrl

이제 우리 API는 준비 되었고, Heroku 안에 호스팅되어있습니다.
Heroku for Node에 대한 세팅 과정에 대한 정보가 필요할 경우에 역시나 설명서를 확인하세요.

이제 우리 Todo API는 준비었고, 어떠한 front end framework를 가지고 Todo Application을 빌드 할수 있습니다.

Running test cases

이제, 이라는 우리 컨트롤 로직과 단위 테스트 케이스 준비를 마쳤습니다.
여러분의 터미널에서 아래 결과를 얻기 위해서 npm test를 사용하세요

테스트가 실패할 경우, 테스트 통과할때 까지 확실하게 수정해주세요.

Integration test for Todo API

다음 테스트는 통합 테스트가 될것입니다.
우리는 test 데이터베이스에 연결하고, 실제 Todo의 기능성에 대해서 체크 해야 합니다.

그래서 우리는 grunt-env를 우리 application에 대한 환경변수로 설정하기 위해 사용할 것이랍니다.

gruntfile.js 
module.exports function(grunt) {

   grunt.initConfig({
      nodemon: {
         dev: {
            script'server.js'
         }
      },
      simplemocha: {
         all: {
            src: ['test/**/*.js']
         },
         unitTest: {
            src: ['test/test.js']
         },
         integrationtest: {
            src: ['test/integration/*.js']
         }
      },
      env: {
         options: {

         },
         test: {
            NODE_ENV'test'
         },
         prod: {
            NODE_ENV'production'
         }
      }

   });

   grunt.loadNpmTasks('grunt-nodemon');
   grunt.loadNpmTasks('grunt-simple-mocha');
   grunt.loadNpmTasks('grunt-env');
   
   grunt.registerTask('default'['nodemon']);
   // grunt.registerTask('test', ['env:test', 'simplemocha']);
   grunt.registerTask('test'['env:test''simplemocha:unitTest']);
   grunt.registerTask('integrationTest'['env:test''simplemocha:integrationTest']);

};

이제 테스트 환경에서, 데이터베이스는 todoapi_test에 연결될 것입니다.
통합 테스트를 작성해보도록 합시다.
HTTP assertion에 대해서, 우리는 supertest 라이브러리를 사용할것 이랍니다.


"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(errresults){
            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(errresults){
            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(errresult){
            id = result.body.todo._id;
            done();
         })
      });

      it('Should delete the todo by _id'function (done) {
         agent
         .delete('/api/todos/'+id)
         .end(function(errresult){
            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(errresult){
            id = result.body.todo._id;
            done();
         })
      });

      it('Should update the completed status of todo by _id to true'function (done) {
         var params = { completedtrue };
         agent
         .put('/api/todos/'+id)
         .send(params)
         .end(function(errresult){
            result.body.status.should.equal(true);
            done();
         })

      });
   });

});


유닛 테스트 케이스를 마쳤습니다.
아래 결과를 얻기 위해서, grunt integrationTest 를 사용하세요

Todo
- code coverage 측정을 위한 Travis 와 Codecov 통합하기 

conclustion

우리는 지금 유닛과 통합 테스트를 마쳐서, Node와 MongoDB를 이용해서 기본 RESTfulTodo API를 갖게 되었습니다. 
질문 있으면 아래 comment 남겨주세요.




이상 
























'JS&FRONT > NodeJs' 카테고리의 다른 글

Node.js 다시한번 공부 해보자....랑 잡소리  (0) 2015.06.11
무료 Grid 와 Chart  (0) 2013.11.05
JAVASCRIPT 정규식 유효성 검사  (0) 2013.07.30
Npm (node package Manager)  (0) 2013.06.29
OOP, Core Javascript  (0) 2013.06.27
NODE.JS 해보자  (0) 2013.06.27