2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

express のミドルウェアを単体試験する

Posted at

ミドルウェアの単体試験は何を試験するの?

Node.js + express で Web アプリケーションを構築する際、ミドルウェアの単体試験についてまとまった情報が見つからずハマりました。
そもそもミドルウェアで行われる処理は、「この条件の時はエラーなので res.status(400).render('./error_page') して終了」というようなことが多いです。
したがって試験の網羅率を上げるには、これらのメソッドが呼ばれたことを検出する必要があります。
next() ぐらいなら単純に sinon.spy() で検出できますが、res.status().render() のようなチェーンメソッドの場合には工夫が必要だったので、試行錯誤した結果をまとめます。

各種前提

ソフトウェアバージョン

  • node.js v12.17.0
  • express 4.17.1
  • chai 4.2.0
  • sinon 9.0.1
  • mocha 8.8.1

本記事のスコープ

  • npm モジュール chai, mocha,sinon を用いた express ミドルウェアの単体試験方法。

本記事のスコープではないもの

  • node.js に関する基礎知識。
  • express のミドルウェアに関すること。
  • npm モジュール chai, mocha, sinon に関する基礎知識。
  • Promise なミドルウェア。

ミドルウェアと単体試験コードの作成

ミドルウェアの作成

リクエストヘッダにパラメータ authorization が存在するかどうかをチェックするミドルウェアを想定します。

routes/middlewares/checkHeader.js
'use strict';
const checkHeader = () => {
  return (req, res, next) => {
    if (!req.headers.authorization) {
      return res.status(400).json({ message: 'invalid request' });
    }
    return next();
  }
}
module.exports = checkHeader;

試験項目

  • 正常系
    • next() が1回呼ばれること。
    • res.status()res.json() が呼ばれないこと。
  • 異常系
    • res.status() が1回呼ばれること。
    • res.status() の引数が 400 であること。
    • res.json() が1回呼ばれること。
    • next() が呼ばれないこと。

テストの作成

基本的な構造

まずは next() が1回呼ばれることを確認するだけのテストを作成します:

test/unit/routes/middlewares/checkHeader.test.js
/* eslint-env mocha */
'use strict';

const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');

const checkHeader = require('../../../../routes/middlewares/checkHeader.js');

describe('checkHeader の単体試験', () => {
  describe('正常系', () => {
    it('リクエストヘッダーに authorization が存在する', (done) => {
      const req = {};
      req.headers = { authorization: 'something' };
      const res = {};
      const next = sinon.spy();

      // ミドルウェアの実行
      checkHeader()(req, res, next);

      // アサーション
      assert(next.calledOnce);

      done();
    });
  });
});

メソッドチェーンの対処

res.status().json() のようなメソッドチェーンのテストには、sinon.spy() を仕込んだクラスを使います:

class responseSpy {
  constructor() {
    this.statusSpy = sinon.spy();
    this.jsonSpy = sinon.spy();
  }
  status(arg) {
    this.statusSpy(arg);
    return this;
  }
  json() {
    this.jsonSpy();
    return this;
  }
}

このクラスのインスタンスを res に渡せば、res.status()res.json() のコール状況が確認できます:

assert(res.statusSpy.notCalled);// res.status() が呼ばれていないこと
assert(res.jsonSpy.notCalled);// res.json() が呼ばれていないこと

今回は responseSpy クラスに実装したメソッドは status()json() だけです。
必要に応じて send()render() 等を実装してください。

最終的なテストは次の通りです:

test/unit/routes/middlewares/checkHeader.test.js
/* eslint-env mocha */
'use strict';

const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');

// テスト対象のミドルウェアを読み込む
const checkHeader = require('../../../../routes/middlewares/checkHeader.js');

// res に使うクラスを作成
class responseSpy {
  constructor() {
    this.statusSpy = sinon.spy();
    this.jsonSpy = sinon.spy();
  }
  status(arg) {
    this.statusSpy(arg);
    return this;
  }
  json() {
    this.jsonSpy();
    return this;
  }
}

describe('checkHeader の単体試験', () => {
  describe('正常系', () => {
    it('リクエストヘッダーに authorization が存在する', (done) => {
      // ミドルウェアが使う req, res, next を定義
      const req = {};
      req.headers = { authorization: 'something' };
      const res = new responseSpy();// 先に定義したクラスをインスタンス化
      const next = sinon.spy();

      // ミドルウェアの実行
      checkHeader()(req, res, next);

      // アサーション
      assert(next.calledOnce);
      assert(res.statusSpy.notCalled);// res.status() が呼ばれていないこと
      assert(res.jsonSpy.notCalled);// res.json() が呼ばれていないこと

      done();
    });
  });
  
  describe('異常系', () => {
    it('リクエストヘッダーに authorization が存在しない', (done) => {
      const req = {};
      req.headers = {};
      const res = new responseSpy();
      const next = sinon.spy();

      checkHeader()(req, res, next);

      assert(res.statusSpy.calledOnce);// res.status() が1回だけ呼ばれていること
      assert(res.statusSpy.withArgs(400));// res.status() の引数が 400 であること
      assert(res.jsonSpy.calledOnce);// res.json() が1回だけ呼ばれていること
      assert(next.notCalled);

      done();
    });
  });
});

テスト実行

テストを実行すると次のように成功するはずです。

$ npx mocha --exit ./test/unit/routes/middlewares/checkHeader.test.js


  checkHeader の単体試験
    正常系
      ✓ リクエストヘッダーに authorization が存在する
    異常系
      ✓ リクエストヘッダーに authorization が存在しない


  2 passing (22ms)

雑記

  • ミドルウェアは単体ではなく結合で試験するのでは?
    • いやいや単体でしょう。
  • 単体試験で next()res.status() を引っ掛けるというのはアリなのだろうか?
    • ミドルウェアだと条件分岐に入ったら next() して終わり、ということが多いし・・・仕方ないのでは?
  • ミドルウェアで Promise 使う時、例えば DB 参照する時に next() しても、なぜか次のミドルウェアに進んでくれないことがある。return next() すると期待した動作をする。あれ何なんだろう。
  • とりあえずミドルウェアで next(), res.render() 等をするときは明示的に return するようにしているが、これが果たして良いことなのか調査中。
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?