アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

ES6 コードをテストする

ES6 で書かれたコードをユニット テストしたい。できればテスト自体も ES6 で。という希望を実現してくれそうなツールがあったので試してみる。

mocha

ユニット テストには mocha を利用する。業務で Node モジュールのテストに利用していて馴染みがあるのと後述する espower-babel が mocha を想定しているのがその理由。

mocha を npm testnpm run から利用するならインストールはローカルだけでよい。package.json 管理下にある npm にはパスが通った状態になる。

$ npm i -D mocha

余談だが以下の記事を読んで gulp などもローカルにインストールして実行を npm で抽象化するほうがよいのでは?と考えるようになった。

記事中にもメリットとして説明されているとおり利用者は npm だけ覚えればよい。グローバルな npm 依存を避けられるのもよいことだ。難点は gulp の default タスクを gulp で呼び出すのに比べて npm start はすこし長くてタイプが面倒なぐらいか。些末な問題だが。

espower-babel

espower-babel は ES6 で書かれたコードとユニット テストを実行するための機能を提供してくれる。使い方は以下の記事が参考になる。

初期バージョンではユニット テストのみ ES6 で対象となるコードが ES6 の場合は事前に ES5 変換しておく必要があったらしい。現行のものはどちらも ES6 のままでよいとのこと。ふつうに Node や ES5 のテストを書く感覚で運用できて便利だ。

これもローカルにインストールする。

$ npm i -D espower-babel

mocha と espower-babel を組み合わせて npm test からテストを起動するコマンドを package.jsonscripts へ記述。この定義はプロジェクトのルート直下にある test フォルダ内に置かれたユニット テストを起動する。

{
  "scripts": {
    "test": "mocha --compilers js:espower-babel/guess test/**/*.js"
  }
}

ES6 を ES5 にコンパイルしたものを mocha に渡す、という動作になるようだ。

  • 補足 2015/7/14 私のサンプルではテスト用ファイルを *.test.js と命名しているが、このようにするなら espower-babel に指定するパスも test/**/*.test.js にしたほうがよい。こうするとテストだけで使用するユーティリティ JS を test 内に置いたとしても、それをテスト対象から除外できる。

power-assert

power-assert はシンプルな assert メソッドと豊富なエラー情報を提供してくれる。

私は Node の assert でも equalthrows ぐらいしか利用していない。またユニット テストでは成功より失敗時の情報を詳しく知りたいことが多いので power-assert を採用することにした。なお throwsdoesNotThrow なども標準 assert 互換のインターフェースが提供されている。

power-assert はユニット テストから参照するだけなので、これもローカルにインストールすればよい。

$ npm i -D power-assert

テストを書いてみる

実際にテストを書いてみよう。まずプロジェクトは以下のように構成。

/
├ package.json
├ README.md
├ js/
│ ├ sample.js
│ └ util.js
└ test/
   └ sample.test.js

package.json の定義は以下。

{
  "name": "es6-unit-test",
  "version": "1.0.0",
  "description": "Sample for unit test in ES6.",
  "main": "index.js",
  "scripts": {
    "test": "mocha --compilers js:espower-babel/guess test/**/*.js"
  },
  "keywords": [
    "ES6",
    "test"
  ],
  "author": "akabeko",
  "license": "MIT",
  "devDependencies": {
    "espower-babel": "^3.1.1",
    "mocha": "^2.2.5",
    "power-assert": "^0.11.0"
  }
}

テスト対象を実装する。ES6 modules によるファイル参照も試したいので 2 ファイルを用意した。まずは util.js。例外もテストするため、想定外の型をもつ引数が指定されたときに発行している。

class Util {
  sum( a, b ) {
    if( !( typeof a === 'number' && typeof b === 'number' ) ) {
      throw new Error( 'Invalid argument type of not Number.' );
    }

    return ( a + b );
  }
}

// Singleton
export default new Util();

そういえば以前 ES6 でシングルトンを実現するとき Symbol を利用していた。しかし Flux の dispatcher サンプルなどを読むと単純に classnew して export するだけでよいらしい。シングルトンの是非はさておき最近はこの方式で定義している。

次は sample.js。さきほどの util.jsimport している。また、このスクリプトではクラスだけでなく関数も exportdefault とそれ以外をユニット テスト側で個別に import 可能か試したい。

import Util from './util.js';

export default class Sample {
  sum( a, b ) {
    // ES6 modules を試すため、別クラスのメソッドを呼ぶ
    return Util.sum( a, b );
  }

  exists( arr, target ) {
    if( !( arr && 0 < arr.length && arr.indexOf ) ) { return false; }

    return ( arr.indexOf( target ) !== -1 );
  }
}

// 関数を公開
export function Floor( value ) {
  return Math.floor( value );
}

ユニット テストは以下のように実装。power-assert の assertthrows を利用。

import assert  from 'power-assert';
import Sample  from '../js/sample.js';
import {Floor} from '../js/sample.js';

describe( 'Sample.sum()', () => {
  it( '合計', () => {
    const sample = new Sample();
    const sum    = sample.sum( 1, 2 );
    assert( sum === 3 );
  } );

  it( '不正な型による例外発行', () => {
    assert.throws(
      () => {
        const sample = new Sample();
        sample.sum();
      },
      Error
    );
  } );
} );

describe( 'Floor()', () => {
  it( '整数化', () => {
    const value = Floor( 42.195 );
    assert( value === 42 );
  } );
} );

これら 3 ファイルはすべて ES6 のコードになる。果たして無事にテストが走るだろうか?ドキドキしながら実行。

$ npm test

> es6-unit-test@1.0.0 test .../es6-unit-test/src
> mocha --compilers js:espower-babel/guess test/**/*.js

  Sample.sum()
    ✓ 合計
    ✓ 不正な型による例外発行

  Floor()
    ✓ 整数化

  3 passing (8ms)

ばっちり。では意図的にテストを失敗させてみよう。「合計」で実行している Sample.sum の第一引数を null にしてからテストを再実行するとテストが失敗する。

Sample.sum()
    1) 合計
    ✓ 不正な型による例外発行

  Floor()
    ✓ 整数化

  2 passing (18ms)
  1 failing

  1) Sample.sum() 合計:
     Error: Invalid argument type of not Number.
      at Util.sum (js/util.js:18:13)
      at Sample.sum (js/sample.js:19:17)
      at Context.<anonymous> (test/sample.test.js:8:27)

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

発行された例外メッセージと共に発生箇所が詳細に出力されている。ES6 to 5 変換により行数がズレるかも?と思っていたが適切な位置を指してくれた。実に分かりやすい。

まとめ

Browserify/babelify を採用してからユニット テストについて悩んでいた。mocha だけでは ES6 をテストできない。そのため生成されたファイルに対してテストするとか考えたけど面倒そうで躊躇していた。

今回の方法であれば、Browserify/babelify のようなプリプロセスや周辺環境を意識することなく、直に ES6 コードをテストできるので楽ちん。

今回作成したサンプルを公開しておく。