如何使用Sinonunit testing节点API(Express与Mongo DB)

我正在使用Node创build一个API,但是我很难理解如何正确地unit testingAPI。 API本身使用Express和Mongo(使用Mongoose)。

到目前为止,我已经能够为API端点本身的端到端testing创build集成testing。 我使用supertest,mocha和chai进行集成testing,并使用dotenv在运行时使用testing数据库。 npmtesting脚本在集成testing运行之前设置要testing的环境。 它工作出色。

但是我也想为各种组件(例如控制器function)创buildunit testing。

我热衷于使用Sinon进行unit testing,但我正在努力了解下一步要采取的措施。

我将详细介绍一个通用版本的API,重写为每个人最喜欢的Todos。

该应用程序具有以下目录结构:

api |- todo | |- controller.js | |- model.js | |- routes.js | |- serializer.js |- test | |- integration | | |- todos.js | |- unit | | |- todos.js |- index.js |- package.json 

的package.json

 { "name": "todos", "version": "1.0.0", "description": "", "main": "index.js", "directories": { "doc": "docs" }, "scripts": { "test": "mocha test/unit --recursive", "test-int": "NODE_ENV=test mocha test/integration --recursive" }, "author": "", "license": "ISC", "dependencies": { "body-parser": "^1.15.0", "express": "^4.13.4", "jsonapi-serializer": "^3.1.0", "mongoose": "^4.4.13" }, "devDependencies": { "chai": "^3.5.0", "mocha": "^2.4.5", "sinon": "^1.17.4", "sinon-as-promised": "^4.0.0", "sinon-mongoose": "^1.2.1", "supertest": "^1.2.0" } } 

index.js

 var express = require('express'); var app = express(); var mongoose = require('mongoose'); var bodyParser = require('body-parser'); // Configs // I really use 'dotenv' package to set config based on environment. // removed and defaults put in place for brevity process.env.NODE_ENV = process.env.NODE_ENV || 'development'; // Database mongoose.connect('mongodb://localhost/todosapi'); //Middleware app.set('port', 3000); app.use(bodyParser.urlencoded({extended: true})); app.use(bodyParser.json()); // Routers var todosRouter = require('./api/todos/routes'); app.use('/todos', todosRouter); app.listen(app.get('port'), function() { console.log('App now running on http://localhost:' + app.get('port')); }); module.exports = app; 

serializer.js

(这个纯粹是从Mongo获取输出并将其序列化成JsonAPI格式,所以这个例子有点多余,但是我把它留在了这里,因为我现在正在使用api。)

 'use strict'; var JSONAPISerializer = require('jsonapi-serializer').Serializer; module.exports = new JSONAPISerializer('todos', { attributes: ['title', '_user'] , _user: { ref: 'id', attributes: ['username'] } }); 

routes.js

 var router = require('express').Router(); var controller = require('./controller'); router.route('/') .get(controller.getAll) .post(controller.create); router.route('/:id') .get(controller.getOne) .put(controller.update) .delete(controller.delete); module.exports = router; 

model.js

 var mongoose = require('mongoose'); var Schema = mongoose.Schema; var todoSchema = new Schema({ title: { type: String }, _user: { type: Schema.Types.ObjectId, ref: 'User' } }); module.exports = mongoose.model('Todo', todoSchema); 

controller.js

 var Todo = require('./model'); var TodoSerializer = require('./serializer'); module.exports = { getAll: function(req, res, next) { Todo.find({}) .populate('_user', '-password') .then(function(data) { var todoJson = TodoSerializer.serialize(data); res.json(todoJson); }, function(err) { next(err); }); }, getOne: function(req, res, next) { // I use passport for handling User authentication so assume the user._id is set at this point Todo.findOne({'_id': req.params.id, '_user': req.user._id}) .populate('_user', '-password') .then(function(todo) { if (!todo) { next(new Error('No todo item found.')); } else { var todoJson = TodoSerializer.serialize(todo); return res.json(todoJson); } }, function(err) { next(err); }); }, create: function(req, res, next) { // ... }, update: function(req, res, next) { // ... }, delete: function(req, res, next) { // ... } }; 

testing/单元/ todos.js

 var mocha = require('mocha'); var sinon = require('sinon'); require('sinon-as-promised'); require('sinon-mongoose'); var expect = require('chai').expect; var app = require('../../index'); var TodosModel = require('../../api/todos/model'); describe('Routes: Todos', function() { it('getAllTodos', function (done) { // What goes here? }); it('getOneTodoForUser', function (done) { // What goes here? }); }); 

现在我不想自己testing路由(我在集成testing中这样做不详细)。

我目前的想法是,下一个最好的事情是实际unit testingcontroller.getAll或controller.getOne函数。 然后通过使用Sinon存根,通过Mongoose模拟对Mongo的调用。

但是我不知道下一步该怎么做,尽pipe已经阅读了sinon文档:

问题

  • 如果要求req,res,next作为参数,我如何testing控制器function?
  • 我是否将模型的查找和填充(目前在Controller函数中)移动到todoSchema.static函数中?
  • 如何嘲笑填充函数做一个mongooseJOIN?
  • 基本上什么进入test/unit/todos.js得到上述在一个坚实的unit testing状态:/

最终目标是运行mocha test/unit ,并让它unit testing该API部分的各个部分

嗨,我已经创build了一些testing,以了解如何使用模拟。

完整的例子github / nodejs_unit_tests_example

controller.test.js

 const proxyquire = require('proxyquire') const sinon = require('sinon') const faker = require('faker') const assert = require('chai').assert describe('todo/controller', () => { describe('controller', () => { let mdl let modelStub, serializerStub, populateMethodStub, fakeData let fakeSerializedData, fakeError let mongoResponse before(() => { fakeData = faker.helpers.createTransaction() fakeError = faker.lorem.word() populateMethodStub = { populate: sinon.stub().callsFake(() => mongoResponse) } modelStub = { find: sinon.stub().callsFake(() => { return populateMethodStub }), findOne: sinon.stub().callsFake(() => { return populateMethodStub }) } fakeSerializedData = faker.helpers.createTransaction() serializerStub = { serialize: sinon.stub().callsFake(() => { return fakeSerializedData }) } mdl = proxyquire('../todo/controller.js', { './model': modelStub, './serializer': serializerStub } ) }) beforeEach(() => { modelStub.find.resetHistory() modelStub.findOne.resetHistory() populateMethodStub.populate.resetHistory() serializerStub.serialize.resetHistory() }) describe('getAll', () => { it('should return serialized search result from mongodb', (done) => { let resolveFn let fakeCallback = new Promise((res, rej) => { resolveFn = res }) mongoResponse = Promise.resolve(fakeData) let fakeRes = { json: sinon.stub().callsFake(() => { resolveFn() }) } mdl.getAll(null, fakeRes, null) fakeCallback.then(() => { sinon.assert.calledOnce(modelStub.find) sinon.assert.calledWith(modelStub.find, {}) sinon.assert.calledOnce(populateMethodStub.populate) sinon.assert.calledWith(populateMethodStub.populate, '_user', '-password') sinon.assert.calledOnce(serializerStub.serialize) sinon.assert.calledWith(serializerStub.serialize, fakeData) sinon.assert.calledOnce(fakeRes.json) sinon.assert.calledWith(fakeRes.json, fakeSerializedData) done() }).catch(done) }) it('should call next callback if mongo db return exception', (done) => { let fakeCallback = (err) => { assert.equal(fakeError, err) done() } mongoResponse = Promise.reject(fakeError) let fakeRes = sinon.mock() mdl.getAll(null, fakeRes, fakeCallback) }) }) describe('getOne', () => { it('should return serialized search result from mongodb', (done) => { let resolveFn let fakeCallback = new Promise((res, rej) => { resolveFn = res }) mongoResponse = Promise.resolve(fakeData) let fakeRes = { json: sinon.stub().callsFake(() => { resolveFn() }) } let fakeReq = { params: { id: faker.random.number() }, user: { _id: faker.random.number() } } let findParams = { '_id': fakeReq.params.id, '_user': fakeReq.user._id } mdl.getOne(fakeReq, fakeRes, null) fakeCallback.then(() => { sinon.assert.calledOnce(modelStub.findOne) sinon.assert.calledWith(modelStub.findOne, findParams) sinon.assert.calledOnce(populateMethodStub.populate) sinon.assert.calledWith(populateMethodStub.populate, '_user', '-password') sinon.assert.calledOnce(serializerStub.serialize) sinon.assert.calledWith(serializerStub.serialize, fakeData) sinon.assert.calledOnce(fakeRes.json) sinon.assert.calledWith(fakeRes.json, fakeSerializedData) done() }).catch(done) }) it('should call next callback if mongodb return exception', (done) => { let fakeReq = { params: { id: faker.random.number() }, user: { _id: faker.random.number() } } let fakeCallback = (err) => { assert.equal(fakeError, err) done() } mongoResponse = Promise.reject(fakeError) let fakeRes = sinon.mock() mdl.getOne(fakeReq, fakeRes, fakeCallback) }) it('should call next callback with error if mongodb return empty result', (done) => { let fakeReq = { params: { id: faker.random.number() }, user: { _id: faker.random.number() } } let expectedError = new Error('No todo item found.') let fakeCallback = (err) => { assert.equal(expectedError.message, err.message) done() } mongoResponse = Promise.resolve(null) let fakeRes = sinon.mock() mdl.getOne(fakeReq, fakeRes, fakeCallback) }) }) }) }) 

model.test.js

 const proxyquire = require('proxyquire') const sinon = require('sinon') const faker = require('faker') describe('todo/model', () => { describe('todo schema', () => { let mongooseStub, SchemaConstructorSpy let ObjectIdFake, mongooseModelSpy, SchemaSpy before(() => { ObjectIdFake = faker.lorem.word() SchemaConstructorSpy = sinon.spy() SchemaSpy = sinon.spy() class SchemaStub { constructor(...args) { SchemaConstructorSpy(...args) return SchemaSpy } } SchemaStub.Types = { ObjectId: ObjectIdFake } mongooseModelSpy = sinon.spy() mongooseStub = { "Schema": SchemaStub, "model": mongooseModelSpy } proxyquire('../todo/model.js', { 'mongoose': mongooseStub } ) }) it('should return new Todo model by schema', () => { let todoSchema = { title: { type: String }, _user: { type: ObjectIdFake, ref: 'User' } } sinon.assert.calledOnce(SchemaConstructorSpy) sinon.assert.calledWith(SchemaConstructorSpy, todoSchema) sinon.assert.calledOnce(mongooseModelSpy) sinon.assert.calledWith(mongooseModelSpy, 'Todo', SchemaSpy) }) }) }) 

routes.test.js

 const proxyquire = require('proxyquire') const sinon = require('sinon') const faker = require('faker') describe('todo/routes', () => { describe('router', () => { let expressStub, controllerStub, RouterStub, rootRouteStub, idRouterStub before(() => { rootRouteStub = { "get": sinon.stub().callsFake(() => rootRouteStub), "post": sinon.stub().callsFake(() => rootRouteStub) } idRouterStub = { "get": sinon.stub().callsFake(() => idRouterStub), "put": sinon.stub().callsFake(() => idRouterStub), "delete": sinon.stub().callsFake(() => idRouterStub) } RouterStub = { route: sinon.stub().callsFake((route) => { if (route === '/:id') { return idRouterStub } return rootRouteStub }) } expressStub = { Router: sinon.stub().returns(RouterStub) } controllerStub = { getAll: sinon.mock(), create: sinon.mock(), getOne: sinon.mock(), update: sinon.mock(), delete: sinon.mock() } proxyquire('../todo/routes.js', { 'express': expressStub, './controller': controllerStub } ) }) it('should map root get router with getAll controller', () => { sinon.assert.calledWith(RouterStub.route, '/') sinon.assert.calledWith(rootRouteStub.get, controllerStub.getAll) }) it('should map root post router with create controller', () => { sinon.assert.calledWith(RouterStub.route, '/') sinon.assert.calledWith(rootRouteStub.post, controllerStub.create) }) it('should map /:id get router with getOne controller', () => { sinon.assert.calledWith(RouterStub.route, '/:id') sinon.assert.calledWith(idRouterStub.get, controllerStub.getOne) }) it('should map /:id put router with update controller', () => { sinon.assert.calledWith(RouterStub.route, '/:id') sinon.assert.calledWith(idRouterStub.put, controllerStub.update) }) it('should map /:id delete router with delete controller', () => { sinon.assert.calledWith(RouterStub.route, '/:id') sinon.assert.calledWith(idRouterStub.delete, controllerStub.delete) }) }) }) 

serializer.test.js

 const proxyquire = require('proxyquire') const sinon = require('sinon') describe('todo/serializer', () => { describe('json serializer', () => { let JSONAPISerializerStub, SerializerConstructorSpy before(() => { SerializerConstructorSpy = sinon.spy() class SerializerStub { constructor(...args) { SerializerConstructorSpy(...args) } } JSONAPISerializerStub = { Serializer: SerializerStub } proxyquire('../todo/serializer.js', { 'jsonapi-serializer': JSONAPISerializerStub } ) }) it('should return new instance of Serializer', () => { let schema = { attributes: ['title', '_user'] , _user: { ref: 'id', attributes: ['username'] } } sinon.assert.calledOnce(SerializerConstructorSpy) sinon.assert.calledWith(SerializerConstructorSpy, 'todos', schema) }) }) }) 

在这里输入图像描述