くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログです

ExpressJSフレームワークの一つの単体テストの書き方

はじめに

こんにちは、みんなのマーケットのテックチームのクイです。

前回、ExpressJSフレームワークの紹介という記事で弊社のExpressJSフレームワークを利用している仕方を簡単に紹介しました。今回の続きはExpressJSフレームワークの一つの単体テスト(ユニットテスト)の書き方について紹介します。

単体テスト(ユニットテスト)とは

単体テストはプログラムを構成する部品単位(手続き型プログラミングの関数、クラスのメソッドなど)の動きが正しいかどうか検証するというテストです。

Javascriptにはmochajasmineなどの様々なテスティングフレームワークがあります。今回、mochachaisinonでユニットテストを実装します。

インストール

TypescriptとExpressJSのインストール仕方の参考はこちらです。

npm i --save-dev mocha chai sinon ts-node @types/mocha @types/chai @types/sinon

@types/sinonのバージョンによって使い方が違います。私は@types/sinon@4.3.3を使っています。

環境準備

まずはプロジェクトフォルダにtestフォルダを作成してtestフォルダの中unittestフォルダを作成します。 そして、package.jsonファイルに以下のような設定を追加します。

"scripts": {
    "test": "mocha -r ts-node/register src/**/test.ts",
},

これで、ts-nodeを用いてTypeScriptファイルを直接テストすることができます。

簡単に実装してみましょう

まず、テストしたい関数を作成します。

express/app/logic/example.ts

export function example(): string {
    return "テスト";
}

テストするファイルを宣言します。

express/test/unittest/test_example.ts

import { expect } from "chai";

import { example } from "../../app/logics/example";

describe("Example Test", () => {
    it("Example should return テスト", async () => {
        expect(example()).to.equal("テスト");
    });
});

テストを実行した結果は以下のような出力になります。

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Example Test
    ✓ Example should return テスト


  1 passing (24ms)

上は簡単な例ですが、Dependency Injectionのような複雑なクラスの場合はどうすれば良いでしょう。

SinonでDependency Injection使うクラスをテスト仕方

仮に二つのクラスがあるとします。 VehicleというクラスのstartVehicle()メソッドをテストしたいです。 EngineVehicleに注入されるクラスです。

express/app/logics/engine.ts

export enum EngineStatus {
    Idle = "idle",
    Running = "running"
}

export class Engine {
    private status: EngineStatus = EngineStatus.Idle;

    public getEngineStatus(): EngineStatus {
        return this.status;
    }
}

express/app/logics/vehicle.ts

import { Engine, EngineStatus } from "./engine";

export class Vehicle {
    constructor(
        private engine: Engine
    ) { }

    public getVehicle() {
        if (this.engine.getEngineStatus() === EngineStatus.Idle) {
            return "Vehicle is ready";
        } else {
            return "Vehicle is busy";
        }
    }
}

テストするファイル:

express/test/unittest/test_vehicle.ts

import { expect } from "chai";
import * as sinon from "sinon";

import { Engine, EngineStatus } from "../../app/logics/engine";
import { Vehicle } from "../../app/logics/vehicle";

describe("Test Vehicle", () => {
    let sandbox: sinon.SinonSandbox;
    let vehicle: Vehicle;

    beforeEach(() => {
        sandbox = sinon.createSandbox();
        const engine = sandbox.createStubInstance(Engine);
        engine.setEngineStatus = (status: EngineStatus): EngineStatus => { // setEngineStatusメソッドをモックする。
            return EngineStatus.Running;
        };

        vehicle = new Vehicle(engine);
    });

    afterEach(() => {
        sandbox.restore();
    });

    it("Vehicle should be busy", () => {
        expect(vehicle.getVehicle()).to.equal("Vehicle is busy");
    });
});

テストの結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Test Vehicle
    ✓ Vehicle should be busy


  1 passing (18ms)

テストが失敗するケースを実装してみます。 テストファイルを以下のように修正してください。

it("Vehicle should be busy", async () => {
    expect(vehicle.getVehicle()).to.equal("Vehicle is ready");
});

結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> mocha -r ts-node/register test/**/*.ts



  Test Vehicle
    1) Vehicle should be busy


  0 passing (45ms)
  1 failing

  1) Test Vehicle
       Vehicle should be busy:

      AssertionError: expected 'Vehicle is busy' to equal 'Vehicle is ready'
      + expected - actual

      -Vehicle is busy
      +Vehicle is ready
      
      at Object.<anonymous> (test/unittest/test_vehicle.ts:26:41)
      at next (native)
      at /Users/quynv/Documents/express/test/unittest/test_vehicle.ts:7:71
      at __awaiter (test/unittest/test_vehicle.ts:3:12)
      at Context.it (test/unittest/test_vehicle.ts:25:34)



npm ERR! Test failed.  See above for more details.

カバレッジを導入する

まず、必要なパッケージをインストールします。

npm i --save-dev nyc source-map-support

次にpackage.jsonファイルを以下のように修正します。

  "scripts": {
    "test": "nyc mocha"
  },
  ....
  "nyc": {
    "check-coverage": true,
    "extension": [
      ".ts"
    ],
    "require": [
      "ts-node/register",
      "source-map-support/register"
    ],
    "reporter": [
      "lcov",        //どこのまだ実行されないコードが見える
      "text-summary" //結果テキストのもとに見える
    ],
    "exclude": [     //除きたいフォルダ
      "controllers",
      "vendor",
      "bin",
      "test"
    ],
    "report-dir": "./test/coverage/", //テストの結果のHTMLファイルが保存する場所。
    "sourceMap": true,
    "instrument": true
  }

testフォルダの中mocha.optsファイルを作成して以下のような内容を入力します。

/test/mocha.opts

--r ts-node/register
--r source-map-support/register
--full-trace
--bail
test/**/*.ts

テストの結果:

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> nyc mocha



  Test Vehicle
    ✓ Vehicle should be busy


  1 passing (18ms)

ERROR: Coverage for lines (75%) does not meet global threshold (90%)

=============================== Coverage summary ===============================
Statements   : 75% ( 9/12 )
Branches     : 75% ( 3/4 )
Functions    : 60% ( 3/5 )
Lines        : 75% ( 9/12 )
================================================================================
npm ERR! Test failed.  See above for more details.

全部のテストが成功しましたが、カバレッジの各値が低いように見えます。

test/coverage/lcov-report/index.htmlファイルを見ましょう f:id:curama-tech:20180711181520p:plain vehicle.tsをクリックして f:id:curama-tech:20180711181540p:plain

上の結果を見るとvehicleファイルに実行していないコードがまだあることがわかります。

つまり、テストケースがまだ足りませんでした。修正しましょう。

engine.tsファイルをテストしたくない場合はファイルの上に/* istanbul ignore file */を挿入して、テストするファイルを以下のように修正します。

describe("Test Vehicle", () => {
    let sandbox: sinon.SinonSandbox;
    let vehicle: Vehicle;
    let engine: Engine;

    beforeEach(() => {
        sandbox = sinon.createSandbox();
        engine = sandbox.createStubInstance(Engine);
        engine.getEngineStatus = (): EngineStatus => {
            return EngineStatus.Running;
        };

        vehicle = new Vehicle(engine);
    });

    afterEach(() => {
        sandbox.restore();
    });

    it("Vehicle should be busy", async () => {
        expect(vehicle.getVehicle()).to.equal("Vehicle is busy");
    });

    it("Vehicle should be ready", async () => {
        engine.getEngineStatus = (): EngineStatus => {
            return EngineStatus.Idle;
        };

        vehicle = new Vehicle(engine);
        expect(vehicle.getVehicle()).to.equal("Vehicle is ready");
    });
});

修正したテストの結果

$ npm test

> re-express@1.0.0 test /xxx/xxx/xxx/express
> nyc mocha



  Test Vehicle
    ✓ Vehicle should be busy
    ✓ Vehicle should be ready


  2 passing (15ms)

=============================== Coverage summary ===============================
Statements   : 100% ( 6/6 )
Branches     : 100% ( 2/2 )
Functions    : 100% ( 2/2 )
Lines        : 100% ( 6/6 )
================================================================================

f:id:curama-tech:20180711181529p:plain f:id:curama-tech:20180711181545p:plain

以上です。

最後に

みんなのマーケット達と一緒に働く仲間を募集しています!。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/ )。