2016-07-04 14 views
2

ノードを使用してAPIを作成していますが、APIを適切にユニットテストする方法を理解するのは苦労しています。 API自体は、ExpressとMongo(Mongooseを使用)を使用しています。Sinon(Express with Mongo DB)を使用してノードAPIをテストする方法

これまで、APIエンドポイント自体のエンドツーエンドテスト用の統合テストを作成できました。私はsupertest、mocha、chaiを統合テストに使い、dotenvを実行してテストデータベースを使用しました。 npmテストスクリプトは、統合テストを実行する前に、テストする環境を設定します。それは優秀に働く。

しかし、コントローラ機能などのさまざまなコンポーネントのユニットテストも作成したいと考えています。

ユニットテストではシノンを使いたいと思っていますが、次のステップを踏まえて苦労しています。私は、詳細みんなの好きなトドスに書き換えAPIのgenericisedバージョンをよ

アプリは、以下のディレクトリ構造を有する:

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; 

シリアル

izer.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; 

モデル。 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 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? 
    }); 
}); 

todos.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) { 
     // ... 
    } 
}; 

テスト/ユニット/今、私は(私は統合テストでは、ここでは詳述しないことを行う)ルート自体をテストする必要はありません。

私の現在の考え方は、次善の策は、実際のユニットテストcontroller.getAllまたはcontroller.getOne機能であることです。そして、Sinonスタブを使ってMongoose経由でMongoへの呼び出しをモックする。

しかし、私は何sinonドキュメントを読んだにも関わらず、次に何をするは考えていません:/

質問それはREQを必要とする場合、私は次のパラメータとして、解像度、コントローラの機能をテストするにはどうすればよい

  • を?
  • モデルのfindおよびpopulate(現在Controller関数内にある)をtodoSchema.static関数に移動しますか?
  • Mongoose JOINを実行するためにpopulate関数をモックする方法はありますか?/

最終目標はmocha test/unitを実行することであり、そのAPIセクション

+0

テストAPIのNPMでsupertestモジュールを見てください。 – afuous

+0

@afuous - 応答ありがとう:) ええ私はいくつかの統合テストでAPIエンドポイント自体にヒットするためにSupertestを使用しました。これは、専用のテストデータベースを使用し、実行する前に環境をTESTに設定しました。 ここで私が興味を持っているのは、Sinonのようなものを使って特定の機能(Controller機能など)をテストし、依存関係をアクティブにする必要がないということです(Mongooseのアクションを擬似し、 )。 このような単体テストは、私にとって初めてのことであり、完全な環境からの独立したテスト方法についてはわかりません。 – nodenoob

答えて

2

のHiのさまざまな部分を、それが持っているユニットテスト:

  • は、基本的には何が固体ユニットテスト状態で上記を得るためにtest/unit/todos.jsに入りますモックの使い方を理解するためのテストをいくつか作成しました。

    フル例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) 
        }) 
        }) 
    }) 
    

    enter image description here

  • 関連する問題