はじめに
こんにちは、みんなのマーケットのテックチームのクイです。
前回、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/ )。