首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何用Sinon (用Mongo快速)测试节点API

如何用Sinon (用Mongo快速)测试节点API
EN

Stack Overflow用户
提问于 2016-07-04 18:49:43
回答 1查看 3.9K关注 0票数 7

我正在使用Node创建一个API,但我很难理解如何正确地对API进行单元测试。API本身使用Express和Mongo (与Mongoose一起使用)。

到目前为止,我已经能够为API端点本身的端到端测试创建集成测试。我使用supertest、mocha和chai作为集成测试以及dotenv在运行时使用测试数据库。npm测试脚本在集成测试运行之前设置要测试的环境。它工作得很好。

但是我也想为各种组件创建单元测试,比如控制器功能。

我热衷于在单元测试中使用Sinon,但我很难知道下一步该采取什么步骤。

我将详细介绍API的泛化版本,该API被重写为每个人最喜欢的Todos。

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

代码语言:javascript
运行
复制
api
|- todo
|   |- controller.js
|   |- model.js
|   |- routes.js
|   |- serializer.js
|- test
|   |- integration
|   |  |- todos.js
|   |- unit
|   |  |- todos.js
|- index.js
|- package.json

package.json

代码语言:javascript
运行
复制
{
  "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

代码语言:javascript
运行
复制
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中使用的东西。)

代码语言:javascript
运行
复制
'use strict';

var JSONAPISerializer = require('jsonapi-serializer').Serializer;

module.exports = new JSONAPISerializer('todos', {
    attributes: ['title', '_user']
    ,
    _user: {
        ref: 'id',
        attributes: ['username']
    }
});

routes.js

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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) {
        // ...
    }
};

test/unit/todos.js

代码语言:javascript
运行
复制
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?
  });
});

现在我不想测试路由本身(我在这里不详细的Integration中这样做)。

我目前的想法是,接下来最好的方法是实际测试controller.getAll或controller.getOne函数。然后用西农存根来模拟蒙戈对蒙戈的召唤。

但是,我不知道下一步该怎么做,尽管我已经读过sinon的文档:/

问题

  • 如果需要req,res,next作为参数,如何测试控制器函数?
  • 我是否将模型的查找和填充(目前在Controller函数中)移动到todoSchema.static函数中?
  • 如何模拟填充函数来进行猫鼬连接?
  • 基本上,进入test/unit/todos.js以使上面的内容处于固态单元测试状态:/

最终目标是运行mocha test/unit并让它对API部分的各个部分进行单元测试。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2017-05-30 18:00:23

嗨,我为您创建了一些测试,以了解如何使用模拟。

完整示例示例

controller.test.js

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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)
    })
  })
})

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/38190712

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档