JSXテストチュートリアル

2013/12/23追記:
gfxから頂いた下記のフィードバックにより、修正しました。
TestCase強化しました · Issue #1 · yosuke-furukawa/jsx_test_tutorial · GitHub

gfx++



JSXでテストどう書くのか、という話。

このエントリは、JSXアドベントカレンダーの21日目の記事です。

JSX書く時、あらびき的チュートリアルuupaa的チュートリアルが非常に参考になりますが、テストに関してはもう少し深堀りしておいてもいいのかなと。

JSXテストでHelloWorld

JSXは組み込みでユニットテストの機能があります。

HelloWorldクラスがあるとします。

import "js/nodejs.jsx";

class HelloWorld {

  // 同期的なメソッド
  function hello(arg : string) :string {
    return "hello " + arg;
  }

  // 非同期的なメソッド
  function helloAsync(arg : string, cb:(string)->void) :void {
    process.nextTick(()->{
      cb("hello " + arg);
    });
  }
}


同期的なメソッドと非同期的なメソッドをテストするためのクラスを作成します。

import "hello_world.jsx";
import "test-case.jsx";

class _Test extends TestCase {

  function testHello() : void {
    var helloWorld = new HelloWorld();
    var answer = helloWorld.hello("test");
    this.expect(answer).toBe("hello test");
  }

  function testHelloAsync() : void {
    this.async((async : AsyncContext)->{
      var helloWorld = new HelloWorld();
      helloWorld.helloAsync("test", function(answer){
        this.expect(answer).toBe("hello test");
        async.done();
      });
    }, 10000);
  }

}

見ていただくと分かるように、"test-case.jsx"と呼ばれるモジュールをimportする必要があります。
また、テストクラスにはTestCaseクラスをextendsさせる必要があります。

class _Test extends TestCase {
// some function
}

さらに、実際のテストはtestから始まるfunction名にする必要があります。

  function testHello() : void {
    var helloWorld = new HelloWorld();
    var answer = helloWorld.hello("test");
    // expect.jsっぽく、this.expect(actual).toBe(expect)の形式でチェックする。
    this.expect(answer).toBe("hello test");
  }

非同期のtestの場合は若干特殊で最初にasync関数を呼び出します。

  function testHelloAsync() : void {
    // AsyncContextを引数に取るcallbackを使う
    this.async((async : AsyncContext)->{
      var helloWorld = new HelloWorld();
      helloWorld.helloAsync("test", function(answer){
        // callbackでチェック
        this.expect(answer).toBe("hello test");
        // async#doneを呼び出すことで終了を呼び出す。
        async.done();
      });
        // この10000はtimeoutの時間、10秒待って終わらなかったらtimeoutのエラーになる。
    }, 10000);
  }

AsyncContextを含むcallbackを作成し、最終的にAsyncContext#doneを呼ぶことでテスト終了になります。
と、ここまでがtestチュートリアルの中でよくあるコード。

Assert

通常のAssertは以下のように記述します。

  this.expect(1 + 2).toBe(3)

これは == で評価しているのと同様です。

ちなみに、expectの第二引数に注釈を書くことも可能です。

  this.expect(1 + 2, "expect 3").toBe(3)

書いておくと、test時に注釈付きで出力されます。

     ok 1
     ok 2 - expect 3
toBeGT, toBeLT, toBeGE, toBeLE

これ以外にも値を評価する場合、LessThanやGreaterThan等のメソッドが使えます。

  function testSum() : void {
      var helloWorld = new HelloWorld();
      var answer = helloWorld.sum(1, 2);
      this.expect(answer).toBe(3);
      // answer > 2
      this.expect(answer).toBeGT(2);
      // answer < 4
      this.expect(answer).toBeLT(4);
      // answer >= 3
      this.expect(answer).toBeGE(3);
      // answer <= 3
      this.expect(answer).toBeLE(3);
  }
正規表現マッチ

また、文字列を完全一致だけで評価するのではなく、正規表現でマッチさせることも可能です。
この場合はtoMatch, notToMatchで評価することが可能です。

  function testRegex() : void {
    var helloWorld = new HelloWorld();
    var answer = helloWorld.hello("test");
    this.expect(answer).toMatch(/^hello te.*t$/);
    this.expect(answer).notToMatch(/^te.*t hello$/);
  }
配列マッチ

配列をマッチさせる場合は以下のtoEqualを使う必要があります。

  function testArray() : void {
    this.expect([1,2,3]).toEqual([1,2,3]);
    this.expect(["a",2,3]).toEqual(["a",2,3]);
  }
Mapマッチ

JSX 0.9.72でのエンハンスにより、以下のように書けるようになりました!

    this.expect({"a":"b"}).toEqual({"a":"b"});

MapをマッチさせるためのAssertは存在しませんが、TestCaseクラスのthis.equalsを使うと一致しているかどうかがわかるので、それを利用するとできます。

  function testMap() : void {
    this.expect(this.equals({"a":"b"}, {"a":"b"})).toBe(true);
  }

ただ、これだと期待値と実体値が異なる場合にtrueとfalseが違うとしか出なくて不便です。
やるなら、以下の様なメソッドを用意しておくと捗りそうな気がします。

  function toMapMatch(expect : Map.<variant>, actual : Map.<variant>) : void {
    var isEquals = this.equals(expect, actual);
    if (!isEquals) {
      log "NOT MATCHED : ";
      log expect;
      log actual;
    }
    this.expect(isEquals).toBe(true);
  }

pass/fail

テストを明示的に成功/失敗させたい時のメソッドです。
こんな感じで使います。

  function testPass() : void {
    try {
      throw new Error("avoid fail");
      this.fail("Should not be reached here");
    } catch (e : Error) {
      this.pass("Success!");
    }
  }

よく使うのはfailで、エラー系のテストとかで例外が上がる事を期待し、「例外が上がらなかったらfailさせる」、とかで使います。
(ちょうど上に書いたテストコードのような例かな。)

setUp/tearDown

テストの事前処理、事後処理に使うメソッドです。

毎回のtest関数の開始前にsetUpが呼び出され、終了時にtearDownが呼び出されます。

以下のようにしておくと、必ずtest関数の開始前にSET UPと表示され、終わりにTEAR DOWNと表示されます。

  override function setUp() : void {
    log "SET UP call before test : ";
  }

  override function tearDown() : void {
    log "TEAR DOWN call after test : ";
  }

実行結果:

SET UP call before test :
     ok 1
TEAR DOWN call after test :
     1..1
ok 1 - testHello

非同期のテストの場合はsetUp(async:AsyncContext)/tearDown(async:AsyncContext)を使うべし。

JSX 0.9.72でのエンハンスにより、非同期のテスト実行時のsetUpとtearDownを書けるようになりました!

  override function setUp(async:AsyncContext) : void {
    log "SET UP call before test : " + async.name();
  }

  override function tearDown(async:AsyncContext) : void {
    log "TEAR DOWN call after test : " + async.name();
  }



非同期のテストを書く場合、setUpが呼ばれるタイミングはtest関数の呼び出される前ですが、実際にtestするのはasync関数からcallbackで呼ばれるときなので、タイミングが異なります。この時に同期的なテストと非同期的なテストを同じテストクラスに記述していると、tearDownがcallされた後、setUpが呼ばれずに非同期のテストが実行されてしまい、テストが期待しない動作をすることがあります。

これを避ける場合は、setUpを明示的にasync関数の中で呼ぶといいです。

  function testHelloAsync() : void {
    this.async((async : AsyncContext)->{
      //ここで明示的にsetUpを呼ぶ!
      this.setUp();
      var helloWorld = new HelloWorld();
      helloWorld.helloAsync("test", function(answer){
        this.expect(answer).toBe("hello test");
        async.done();
      });
    }, 10000);
  }



番外編、テスト書く時のgrunt-jsxの使い方。

テスト書く時、grunt-jsxは以下のようにしておくと tフォルダ以下の*.jsxファイルを全てtestしてくれます。

module.exports = function(grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    env: {
      search_path: ['lib'],
    },
    jsx: {
      test: {
        src: grunt.option('target') || 't/**/*.jsx',
        add_search_path: "<%= env.search_path %>",
        test : true,
      },
    },
  });

  grunt.loadNpmTasks("grunt-jsx");

  grunt.registerTask('default', ['jsx:test']);
}

でテストするときは、

# 以下の書き方でt以下のjsxファイルを全てテストする。
$ grunt jsx:test

# 個別にファイルをテストするときは--targetを付ける
$ grunt jsx:test --target='t/hello_test.jsx'


な感じで個別にもテストできます。

追記:proveを使ったほうがテストは便利

jsx --test を直接使うのではなく、 prove(1) を介してテストするのがお勧めです。
そのほうが -j で並行にテストできますし、テストの出力もシンプルにできます。

ということなので、proveを使った方も紹介しておきます。

prove を使う場合、以下のように --exec でjsxのコマンドを指定する必要があります。で、--extで拡張子をjsxにすると、t以下のフォルダに有るテストを実行してくれます。

$ prove --exec "jsx --test --add-search-path lib" --ext .jsx

実行結果:
f:id:yosuke_furukawa:20131223122636p:plain

毎回指定するのが面倒、って場合は、以下のように.provercに記述しておくと、proveだけで実行できます。

--exec "jsx --add-search-path lib --test"
--ext .jsx

まとめ

  • JSXにはユニットテストの機能が組み込まれており、それを使うと簡単にテストが可能です。
  • ドキュメントに書かれている以外にも多くの機能があるので、テストを書く場合は一度このチュートリアルに目を通すといいかも。
  • gruntで書くと効率的に書けます。
  • prove使うと全体テストがもっといい感じに。

今回使ったテストは以下にまとめました。

https://github.com/yosuke-furukawa/jsx_test_tutorial

Enjoy JSX TESTS!!