Expressを使用して1つのユニットでルートをどのようにテストしますか?


99

私はNode.jsを学習している最中で、Expressで遊んでいます。本当にフレームワークが好きですが、ルートのユニット/統合テストを書く方法を理解するのに苦労しています。

単純なモジュールを単体テストできることは簡単で、Mochaでそれを行ってきました。ただし、渡した応答オブジェクトは値を保持しないため、Expressを使用した単体テストは失敗します。

テスト中のルート関数(routes / index.js):

exports.index = function(req, res){
  res.render('index', { title: 'Express' })
};

単体テストモジュール:

var should = require("should")
    , routes = require("../routes");

var request = {};
var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        viewName = view;
        data = viewData;
    }
};

describe("Routing", function(){
    describe("Default Route", function(){
        it("should provide the a title and the index view name", function(){
        routes.index(request, response);
        response.viewName.should.equal("index");
        });

    });
});

これを実行すると、「エラー:グローバルリークが検出されました:viewName、データ」で失敗します。

  1. これを機能させるためにどこで問題がありますか?

  2. このレベルでコードを単体テストするためのより良い方法はありますか?

更新 1.最初に「it()」を忘れたため、コードスニペットを修正しました。

回答:


21

応答オブジェクトを変更します。

var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        this.viewName = view;
        this.data = viewData;
    }
};

そしてそれはうまくいきます。


2
これは、ルートではなく、リクエストハンドラの単体テストです。
ジェイソンセブリング

43

他の人がコメントで推奨しているように、Expressコントローラーをテストする標準的な方法はsupertestを経由しているようです。

テストの例は次のようになります。

describe('GET /users', function(){
  it('respond with json', function(done){
    request(app)
      .get('/users')
      .set('Accept', 'application/json')
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        done()
      });
  })
});

利点:スタック全体を一度にテストできます。

欠点:統合テストのように感じられ、動作します。


1
私はこれが好きですが、(元の質問のように)viewNameをアサートする方法はありますか?または、応答のコンテンツをアサートする必要がありますか?
Alex

19
私はあなたの欠点に同意します、これはユニットテストではありません。これは、すべてのユニットの統合に依存して、アプリケーションのURLをテストします。
ルークH

10
「ルート」は本当にでありintegration、おそらくテストルートは統合テストに委ねられるべきだと言うのは合法だと思います。つまり、定義されたコールバックに一致するルートの機能は、おそらくexpress.jsによってすでにテストされています。ルートの最終結果を取得するための内部ロジックは、理想的にはルートの外部でモジュール化されている必要があり、それらのモジュールはユニットテストされる必要があります。それらの相互作用、つまりルートは、統合テストする必要があります。同意しますか
Aditya MP

1
エンドツーエンドのテストです。間違いない。
kgpdeveloper 2018年

23

エクスプレスアプリケーションを実際に単体テストする唯一の方法は、リクエストハンドラーとコアロジックの間の分離を維持することであるという結論に達しました。

したがって、アプリケーションロジックは、requiredおよび単体テストが可能な個別のモジュール内にある必要があり、Express RequestクラスやResponseクラスへの依存度が最小限である必要があります。

次に、リクエストハンドラーで、コアロジッククラスの適切なメソッドを呼び出す必要があります。

現在のアプリの再構築が完了したら、例を上げます。

こんな感じかな(要旨やコメントを自由にフォークしてください。まだ調査中です)。

編集する

これが小さな例です。より詳細な例については、要旨を参照してください。

/// usercontroller.js
var UserController = {
   _database: null,
   setDatabase: function(db) { this._database = db; },

   findUserByEmail: function(email, callback) {
       this._database.collection('usercollection').findOne({ email: email }, callback);
   }
};

module.exports = UserController;

/// routes.js

/* GET user by email */
router.get('/:email', function(req, res) {
    var UserController = require('./usercontroller');
    UserController.setDB(databaseHandleFromSomewhere);
    UserController.findUserByEmail(req.params.email, function(err, result) {
        if (err) throw err;
        res.json(result);
    });
});

3
私の意見では、これは使用するのに最適なパターンです。言語間の多くのWebフレームワークは、コントローラーパターンを使用して、ビジネスロジックを実際のhttp応答形成機能から分離します。この方法では、http応答プロセス全体ではなくロジックだけをテストできます。これは、フレームワークの開発者が独自にテストする必要があることです。このパターンでテストできる他のことは、単純なミドルウェア、いくつかの検証関数、およびその他のビジネスサービスです。ただし、DB接続テストはまったく異なるタイプのテストです
OzzyTheGiant

1
実際、ここでの答えの多くは、統合/機能テストに関するものです。
ルークH

19

ExpressでHTTPをテストする最も簡単な方法は、TJのhttpヘルパーを盗むことです。

私は個人的に彼のヘルパーを使用しています

it("should do something", function (done) {
    request(app())
    .get('/session/new')
    .expect('GET', done)
})

ルートオブジェクトを具体的にテストする場合は、正しいモックを渡します

describe("Default Route", function(){
    it("should provide the a title and the index view name", function(done){
        routes.index({}, {
            render: function (viewName) {
                viewName.should.equal("index")
                done()
            }
        })
    })
})

5
「ヘルパー」リンクを修正できますか?
ニコラスマレー

16
HTTPユニットテストへのより最新のアプローチは、Visionmediaによるスーパーテストを使用することです。また、TJのhttpヘルパーがスーパーテストに進化したようです。
AkseliPalén2013年

2
githubのスーパーテストはここにあります
Brandon

@Raynosでは、例の中でリクエストとアプリを取得する方法を説明できますか?
jmcollin92

9
残念ながら、これは単体テストではなく統合テストです。
ルークH

8

Express 4での単体テストの場合、gjohnsonの次の例に注意してください。

var express = require('express');
var request = require('supertest');
var app = express();
var router = express.Router();
router.get('/user', function(req, res){
  res.send(200, { name: 'tobi' });
});
app.use(router);
request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '15')
  .expect(200)
  .end(function(err, res){
    if (err) throw err;
  });

1

私もこれを疑問に思っていましたが、特に統合テストではなく単体テスト用です。これは私が今やっていることです、

test('/api base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/api');
});


test('Subrouters loaded', function onTest(t) {
  t.plan(1);

  var router = routerObj.router;

  t.equals(router.stack.length, 5);
});

ここでrouterObjはちょうど{router: expressRouter, path: '/api'}です。次に、サブルーターを読み込んでvar loginRouterInfo = require('./login')(express.Router({mergeParams: true}));から、エクスプレスアプリがエクスプレスルーターをパラメーターとして受け取ってinit関数を呼び出します。次にinitRouterが呼び出しrouter.use(loginRouterInfo.path, loginRouterInfo.router);てサブルーターをマウントします。

サブルーターは以下でテストできます:

var test = require('tape');
var routerInit = require('../login');
var express = require('express');
var routerObj = routerInit(express.Router());

test('/login base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/login');
});


test('GET /', function onTest(t) {
  t.plan(2);

  var route = routerObj.router.stack[0].route;

  var routeGetMethod = route.methods.get;
  t.equals(routeGetMethod, true);

  var routePath = route.path;
  t.equals(routePath, '/');
});

3
これは本当に面白いですね。これがすべてどのように組み合わさっているかを示すために、不足している部分のより多くの例がありますか?
cjbarth

1

統合テストではなく単体テストを実現するために、リクエストハンドラーの応答オブジェクトをモックしました。

/* app.js */
import endpointHandler from './endpointHandler';
// ...
app.post('/endpoint', endpointHandler);
// ...

/* endpointHandler.js */
const endpointHandler = (req, res) => {
  try {
    const { username, location } = req.body;

    if (!(username && location)) {
      throw ({ status: 400, message: 'Missing parameters' });
    }

    res.status(200).json({
      location,
      user,
      message: 'Thanks for sharing your location with me.',
    });
  } catch (error) {
    console.error(error);
    res.status(error.status).send(error.message);
  }
};

export default endpointHandler;

/* response.mock.js */
import { EventEmitter } from 'events';

class Response extends EventEmitter {
  private resStatus;

  json(response, status) {
    this.send(response, status);
  }

  send(response, status) {
    this.emit('response', {
      response,
      status: this.resStatus || status,
    });
  }

  status(status) {
    this.resStatus = status;
    return this;
  }
}

export default Response;

/* endpointHandler.test.js */
import Response from './response.mock';
import endpointHandler from './endpointHander';

describe('endpoint handler test suite', () => {
  it('should fail on empty body', (done) => {
    const res = new Response();

    res.on('response', (response) => {
      expect(response.status).toBe(400);
      done();
    });

    endpointHandler({ body: {} }, res);
  });
});

次に、統合テストを実現するために、endpointHandlerをモックし、supertestを使用してエンドポイントを呼び出すことができます。


0

私の場合、テストしたいのは、正しいハンドラーが呼び出されたかどうかだけです。スーパーテストを使用して、ルーティングミドルウェアへのリクエストの作成の単純さを平均化したかったのです。私はTypescript aを使用していますが、これは私のために働いたソリューションです

// ProductController.ts

import { Request, Response } from "express";

class ProductController {
  getAll(req: Request, res: Response): void {
    console.log("this has not been implemented yet");
  }
}
export default ProductController

ルート

// routes.ts
import ProductController  from "./ProductController"

const app = express();
const productController = new ProductController();
app.get("/product", productController.getAll);

テスト

// routes.test.ts

import request from "supertest";
import { Request, Response } from "express";

const mockGetAll = jest
  .fn()
  .mockImplementation((req: Request, res: Response) => {
    res.send({ value: "Hello visitor from the future" });
  });

jest.doMock("./ProductController", () => {
  return jest.fn().mockImplementation(() => {
    return {
      getAll: mockGetAll,

    };
  });
});

import app from "./routes";

describe("Routes", () => {
  beforeEach(() => {
    mockGetAll.mockImplementation((req: Request, res: Response) => {
      res.send({ value: "You can also change the implementation" });
    });
  });

  it("GET /product integration test", async () => {
    const result = await request(app).get("/product");

    expect(mockGetAll).toHaveBeenCalledTimes(1);

  });



  it("GET an undefined route should return status 404", async () => {
    const response = await request(app).get("/random");
    expect(response.status).toBe(404);
  });
});

モックを機能させるにはいくつかの問題がありました。しかし、jest.doMockと例にある特定の順序を使用すると、動作します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.