简体   繁体   English

如何使用Sinon对单元测试节点API(Express with Mongo DB)

[英]How to Unit Test a Node API using Sinon (Express with Mongo DB)

I am creating an API using Node but am struggling to understand how to properly Unit test the API. 我正在使用Node创建API,但我很难理解如何正确地对API进行单元测试。 The API itself uses Express and Mongo (with Mongoose). API本身使用Express和Mongo(使用Mongoose)。

So far I have been able to create Integration tests for end to end testing of the API endpoints themselves. 到目前为止,我已经能够为API端点本身的端到端测试创建集成测试。 I have used supertest, mocha and chai for the integration tests along with dotenv to use a test database when running it. 我使用supertest,mocha和chai进行集成测试以及dotenv在运行时使用测试数据库。 The npm test script sets the environment to test before the integration tests run. npm测试脚本在集成测试运行之前设置要测试的环境。 It works excellently. 它工作得很好。

But I would like to also create Unit Tests for various components such as the controller functions. 但我还想为各种组件创建单元测试,例如控制器功能。

I'm keen to use Sinon for the Unit Tests but I'm struggling to know what next steps to take. 我很想将Sinon用于单元测试,但我很难知道下一步要采取什么措施。

I'll detail a genericised version of the API rewritten to be everybody's favourite Todos. 我将详细介绍API的通用版本,重写为每个人最喜欢的Todos。

The app has the following directory structure: 该应用程序具有以下目录结构:

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

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 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 serializer.js

(This purely takes the output from Mongo and serializes it into JsonAPI format. So it is a bit superfluous to this example but I left it in as it is something I currently make use of in the api.) (这纯粹是从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 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 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 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) {
        // ...
    }
};

test/unit/todos.js 测试/单元/ 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?
  });
});

Now I don't want to test the routes themselves (I do that in the Integration Tests not detailed here). 现在我不想自己测试路由(我在这里没有详细介绍的集成测试中这样做)。

My current thinking is that the next best thing is to actually unit test controller.getAll or controller.getOne functions. 我目前的想法是,下一个最好的事情是实际单元测试controller.getAll或controller.getOne函数。 And then to Mock the calls to Mongo via Mongoose using Sinon stubs. 然后使用Sinon存根通过Mongoose模拟对Mongo的调用。

But I have no idea what to do next despite having read the sinon docs :/ 但是我不知道接下来要做什么,尽管已经阅读了sinon文档:/

Questions 问题

  • How do I test controller functions if it requires req, res, next as parameters? 如果需要req,res,next作为参数,如何测试控制器功能?
  • Do I move the model's find and populate (currently in the Controller function) into todoSchema.static functions? 我是否将模型的查找和填充(当前在Controller函数中)移动到todoSchema.static函数中?
  • How to mock the populate function to do a Mongoose JOIN? 如何模拟populate函数来做一个Mongoose JOIN?
  • Basically what goes into test/unit/todos.js to get the above in a solid Unit Test state :/ 基本上是什么进入test/unit/todos.js以获得上面的固定单元测试状态:/

The end goal is to run mocha test/unit and have it unit test the various parts of that API section 最终目标是运行mocha test/unit并让它对该API部分的各个部分进行单元测试

Hi I've created some test for you to understand how to use mocks. 嗨,我已经为您创建了一些测试,以了解如何使用模拟。

Full example github/nodejs_unit_tests_example 完整示例github / nodejs_unit_tests_example

controller.test.js 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 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 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 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)
    })
  })
})

在此输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 当未将回调作为参数传递时,如何使用sinon对node.js中的回调进行单元测试? - How to unit test a callback in node.js using sinon, when callback is not passed as parameter? 使用node / mongo / express的“简单” RESTful API - “Simple” RESTful API using node/mongo/express 无法使用 express API 和节点 JS(Angular4) 将数据发布到 mongo db - Not able to post data to mongo db using express API and node JS(Angular4) Node JS单元测试sinon chai - Node js Unit test sinon chai 如何使用Node JS和Mongo DB进行测试 - How to Load test with Node JS and Mongo DB 我如何使用Sinon和Mocha来对Promise进行单元测试 - How do i unit test promises using sinon, mocha 使用Mocha / chai / sinon进行单元测试Express-如何测试res.send对象的形状? - Unit Testing Express with mocha/chai/sinon - how do I test my res.send object shape? 如何使用node.js和express.js向Mongo DB Atlas发出发布请求 - How to make a post request to mongo db atlas using node.js and express.js 如何使用无服务器在 Node js express 服务器中建立与 mongo db 的连接? - How to establish connection to mongo db in a Node js express server using serverless? 使用 sinon 对带有 got 模块的函数进行单元测试 - Unit Test for a function with got module using sinon
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM