はじめに
こんにちは、みんなのマーケットのテックチームのクイです。
前回、ExpressJSフレームワークの紹介という記事で弊社のExpressJSフレームワークを利用している仕方を簡単に紹介しました。今回の続きはExpressJSフレームワークの一つの単体テスト(ユニットテスト)の書き方について紹介します。
単体テスト(ユニットテスト)とは
単体テストはプログラムを構成する部品単位(手続き型プログラミングの関数、クラスのメソッドなど)の動きが正しいかどうか検証するというテストです。
Javascriptにはmocha、jasmineなどの様々なテスティングフレームワークがあります。今回、mochaとchaiとsinonでユニットテストを実装します。
インストール
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()メソッドをテストしたいです。
EngineがVehicleに注入されるクラスです。
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ファイルを見ましょう
vehicle.tsをクリックして

上の結果を見ると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 )
================================================================================

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