【譯】測試驅動開發:使用 Node.js 和 MongoDB 構建 Todo API

本文轉載自:衆成翻譯
譯者:文藺
連接:http://www.zcfy.cc/article/746
原文:https://semaphoreci.com/community/tutorials/a-tdd-approach-to-building-a-todo-api-using-node-js-and-mongodbnode

學習如何使用測試驅動開發的方式,用 Node.js、MongoDB、Mocha 和 Sinon.js 開發 Todo API。git


簡介

測試是軟件開發過程當中的一個完整部分,它幫助咱們提高軟件品質。有不少種測試方法,如手動測試,集成測試,功能測試,負載測試,單元測試等等。在本文中,咱們將會遵循測試驅動開發的規則編寫代碼。程序員

單元測試是什麼?

Martin Fowler 將單元測試定義以下:github

  • 首先一個概念,單元測試是低層次的,專一於軟件系統的一小部分;web

  • 其次,單元測試一般是由程序員使用常規工具本身編寫的 —— 惟一的區別是使用某種單元測試框架;mongodb

  • 再次,單元測試預計比其餘類型的測試顯著地更快。數據庫

在本教程中,咱們將會使用 Node.js 和 MongoDB 構建一個 Todo API。咱們首先會給生產代碼寫單元測試,而後纔會真正寫生產代碼。express

環境

  • Express.jsnpm

  • MongoDBjson

  • Mocha

  • Chai

  • Sinon.js

項目設置

在咱們真正開發 API 以前,咱們必須設置文件夾和端點(end point)。

在軟件項目中,沒有最好的應用架構。本教程使用的文件結構,請看該 GitHub 倉庫。

如今來建立端點(endpoints):

table

安裝依賴

Node.js 有本身的包管理工具 NPM。要學習更多關於 NPM 的知識,能夠看咱們的另外一篇教程,《Node.js Package Manager tutorial》

好,咱們來安裝項目依賴。

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

定義 Schema

咱們會使用 Mongoose 做爲 Node.js 中的對象文檔模型(Object Document Model),它工做起來和典型的 ORM同樣,就像 Rails 中用 ActiveRecord同樣。Mongoose 幫咱們更方便地訪問 MongoDB 命令。首先咱們爲 Todo API 定義 schema。

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;

Mongoose 中的一切都是從 schema 開始。每一個 schema 對應一個 MongoDB 集合,它定義了集合中文檔的形狀。

在上面的 todo schema 中,咱們建立了三個字段來存儲 todo 描述、狀態和建立日期。該 schema 幫助 Node.js 應用理解如何將 MongoDB 中的數據映射成 JavaScript 對象。

搭建 Express Server

咱們將使用 Express 來搭建服務器,它是一個小型 Node.js web 框架,提供了一個強大的功能集,用於開發Web應用程序。

咱們繼續,搭建 Express server。

首先,咱們要按下面這樣引入項目依賴:

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

接着,配置 Express 中間件:

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

管理 Mongoose 鏈接

使用mongoose.connect將 MongoDB 和應用鏈接,這會和數據庫創建鏈接。這就是鏈接 todoapi 數據庫的最小操做,數據庫跑在本地,默認端口是 27017。若是本地鏈接失敗,試試將 localhost 換成 127.0.0.1。

有時候本地主機名改變時會出現一些問題。

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

使用下面的命令啓動服務器:

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

爲 API 編寫測試用例

在 TDD(測試驅動開發)中,將全部可能的輸入、輸出以及錯誤歸入考慮,而後開始編寫測試用例。來給咱們的 Todo API 編寫測試用例吧。

搭建測試環境

以前提到過,咱們會使用 Mocha 做爲測試運行器,Chai 做爲斷言庫,用 Sinon.js 模擬 Todo model。首先安裝單元測試環境:

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

使用 sinon-mongoose 模塊來模擬 Mongoose 定義的 MongoDB 模型。

如今,引入測試的依賴:

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

Todo API 的測試用例

編寫單元測試時,須要同時考慮成功和出錯的場景。

對咱們的 Todo API 來講,咱們要給新建、刪除、更新、查詢 API 同時編寫成功和出錯的測試用例。咱們使用 Mocha, Chai 和 Sinon.js 來編寫測試。

獲取全部 Todo

本小節,咱們來編寫從數據庫獲取全部 todo 的測試用例。須要同時爲成功、出錯場景編寫,以確保代碼在生產中的各類環境下都能正常工做。

咱們不會使用真實數據庫來跑測試用例,而是用 sinon.mock 給 Todo schema 創建假數據模型,而後再測試指望的結果。

來使用 sinon.mock 給 Todo model 據,而後使用 find 方法獲取數據庫中存儲的全部 todo。

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

保存 New Todo

保存一個新的 todo,須要用一個示例任務來模擬 Todo model。使用咱們建立的Todo model來檢驗 mongoose 的save 方法保存 todo 到數據庫的結果。

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

根據 ID 更新 Todo

本節咱們來檢驗 API 的 update 功能。這和上面的例子很相似,除了咱們要使用withArgs方法,模擬帶有參數 ID 的 Todo model。

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

根據 ID 刪除 Todo

這是 Todo API 單元測試的最後一小節。本節咱們將基於給定的 ID ,使用 mongoose 的 remove 方法,測試 API 的 delete 功能。

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

每次咱們都要還原(restore) Todomock,確保下次它還能正常工做。

每次運行測試用例的時候,全部的都會失敗,由於咱們的生產代碼還沒寫好呢。咱們會運行自動化測試,直至全部單元測試都經過。

> 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

你在命令行終端上運行npm test的時候,會獲得上面的輸出信息,全部的測試用例都失敗了。須要根據需求和單元測試用例來編寫應用邏輯,使咱們的程序更加穩定。

編寫應用邏輯

下一步就是爲 Todo API 編寫真正的應用代碼。咱們會運行自動測試用例,一直重構,直到全部單元測試都經過。

配置路由

對客戶端和服務端的 web 應用來講,路由配置是最重要的一部分。在咱們的應用中,使用 Express Router 的實例來處理全部路由。來給咱們的應用建立路由。

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(控制器)

如今咱們差很少在教程的最後階段了,開始來寫控制器代碼。在典型的 web 應用裏,controller 控制着保存、檢索數據的主要邏輯,還要作驗證。來寫Todo API 真正的控制器,運行自動化單元測試直至測試用例所有經過。

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;

運行測試用例

如今咱們完成了應用的測試用例和控制器邏輯兩部分。來跑一下測試,看看最終結果:

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

最終結果顯示,咱們全部的測試用例都經過了。接下來的步驟應該是 API 重構,這包含着重複本教程提到的相同過程。

結論

經過本教程,咱們學習了若是使用測試驅動開發的辦法,用 Node.js and MongoDB 設計 API。儘管 TDD (測試驅動開發)給開發過程帶來了額外複雜度,它能幫咱們創建更穩定的、錯誤更少的應用。就算你不想實踐 TDD, 至少也應該編寫覆蓋應用全部功能點的測試。

若是你有任何問題或想法,請不吝留言。

相關文章
相關標籤/搜索