本文转载自:众成翻译
译者:文蔺
连接: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, 至少也应该编写覆盖应用全部功能点的测试。
若是你有任何问题或想法,请不吝留言。