本文轉載自:衆成翻譯
譯者:文藺
連接: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):
Node.js 有本身的包管理工具 NPM。要學習更多關於 NPM 的知識,能夠看咱們的另外一篇教程,《Node.js Package Manager tutorial》。
好,咱們來安裝項目依賴。
npm install express mongoose method-override morgan body-parser cors —save-dev
咱們會使用 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 來搭建服務器,它是一個小型 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.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
在 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 來講,咱們要給新建、刪除、更新、查詢 API 同時編寫成功和出錯的測試用例。咱們使用 Mocha, Chai 和 Sinon.js 來編寫測試。
本小節,咱們來編寫從數據庫獲取全部 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(); }); }); });
保存一個新的 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(); }); }); });
本節咱們來檢驗 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(); }); }); });
這是 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;
如今咱們差很少在教程的最後階段了,開始來寫控制器代碼。在典型的 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, 至少也應該編寫覆蓋應用全部功能點的測試。
若是你有任何問題或想法,請不吝留言。