カテゴリー別アーカイブ: Dart

DartUnitができるまで

この記事はTDD Advent Calendar jp: 2011 : ATNDへの参加エントリです。昨日は@Gab_kmさんの「#107 TDDに対して思っていること « TDD « Gab_kmのブログ」です。

 

自己紹介

新潟県長岡市のSIerでプログラマーやってます。10月には「TDDBC 長岡 0.1」と銘打って、第23回長岡IT開発者勉強会の中の1セッションとして、TDDについて語らせてもらいました。

第23回長岡IT開発者勉強会でしゃべってきました @masaru_b_cl #nds23 #tddbc

 

Dart言語でTDDやりたい!

そんな「TDDBC 長岡 0.1」でTDDについて発表することになりましたが、テーマが「Googleがらみのなんか」とのこと。これは「Dart言語でTDDデモするしかないだろう」と、誠に安易ながらそう思ったわけです。

しかし、Dart言語はまだ登場して間もない言語。当然ユニットテストを行うフレームワークなんざありません。でも、そこはプログラマーですので、簡易ながらもテストランナーをでっち上げようと思ったんですね。

 

最初の一歩

さて、テストランナーを作ろうと思ったはいいものの、何をどうすればよいでしょう?そんな時は原典に当たりましょう。

はい、ご存知「テスト駆動開発入門 by Kent Beck」でございます。

そのPart.2「xUnitの例」 p.89から、テストランナーに最低限必要な機能についての記述を引用しましょう。

テストメソッドを呼び出す

最初にsetUpを呼び出す

最後にtearDownを呼び出す

テストメソッドが失敗してもtearDownを呼び出す

複数のテストを実行する

収集した結果を報告する

では、最初から実現していってみましょう。

なお、原典ではxUnitをTDDで作ってますが、私はめんどくさくてスルーしました(えー

 

テストメソッドを呼び出す、複数のテストを実行する

まず、大まかに考えましょう。テストメソッドを呼び出す手順は、だいたい次のようになるでしょう。

  1. テストランナーのオブジェクトを作る
  2. 実行したいテストを登録する
  3. テストを実行する

 

テストランナーのオブジェクトを作る

テストを実行するには、まずは次のようにテストランナーのオブジェクトを作るでしょう。

Test
main() {
  var runner = new TestRunner();
}

 

これはもう単純にクラスを定義するだけです。

TestRunner
class TestRunner {
}

 

実行したいテストを登録する

次にテストの登録です。原典ではPythonを使って、「文字列」でテストの名前を登録して実行しています。しかし、Dart言語はラムダ式を用いて、関数をオブジェクトして簡単に扱える機能がありますので、これを使わない手はないでしょう。

具体的には次のような感じで書きたいですね。

Test
main() {
  var runner = new TestRunner();

  var expect = '1';
  var actual = '2';
  runner.add('test description', () => Expect.equals(expect, actual));
}

Expectクラスは、JUnitでいうAssertクラスのようなもので、検証用のメソッドがいくつか用意されています。

 

このインターフェースに合わせてテストランナーを書くと、次のようになります。

TestRunner
class TestRunner {
  // Test Cases
  Map<String, Function> tests;

  // Constructor
  TestRunner() {
    tests = new LinkedHashMap<String, Function>();
  }

  // Add Test Case
  add(message, test) =>
    tests[message] = test;
}

LinkedHashMapを使っているのは、登録したテストケースの順序を保つためです。

 

テストを実行する

最後に登録したテストを実行させます。次のような感じで実行したいですね。

Test
main() {
  var runner = new TestRunner();

  var expect = '1';
  var actual = '2';
  runner.add('test description', () => Expect.equals(expect, actual));

  runner.run();
}

 

今回は簡易的なものなので、テストの実行結果は標準出力に出しちゃいましょう。ということで、テストランナーは次のようになります。

TestRunner
class TestRunner {
  // Test Cases
  Map<String, Function> tests;

  // Constructor
  TestRunner() {
    tests = new LinkedHashMap<String, Function>();
  }

  // Add Test Case
  add(message, test) =>
    tests[message] = test;

  // Run Test Cases
  run() =>
    ((){
      tests.forEach((message,test) {
        try {
          // Run
          test();
          print('${message} : Green!');
        } catch (ExpectException e) {
          // Failure
          print('${message} : Red!');
          print('  ${e}');
        } catch (Exception e) {
          // Error
          print('${message} : Error!');
          print('  ${e}');
        }
      });
    })();
}

Expectクラスの各メソッドは、検証に失敗すると「ExceptException」をthrowするので、それをキャッチすることでテスト失敗を判別しています。

 

最初にsetUpを呼び出す

ここまでで、もっとも簡単な形でテストを実行すること出来るようになりました。これから徐々に肉付けしていきましょう。

まずはsetUpを呼び出せるようにしましょう。

 

setUpもテストケースと同様に、ラムダ式で渡せれば素敵ですよね。

Test
main() {
  var runner = new TestRunner();

  runner.setUp = () => { /* setUp */ };

  var expect = '1';
  var actual = '2';
  runner.add('test description', () => Expect.equals(expect, actual));

  runner.run();
}

 

というわけで、setUpをsetterのみのプロパティとして実装してやります。そして、runメソッド内の最初で呼び出すようにしましょう。

TestRunner
class TestRunner {
  // Test Cases
  Map<String, Function> tests;

  // Constructor
  TestRunner() {
    tests = new LinkedHashMap<String, Function>();
  }

  // Add Test Case
  add(message, test) =>
    tests[message] = test;

  // Set Up
  Function _setUp;
  set setUp(Function func) => _setUp = func;

  // Run Test Cases
  run() =>
    ((){
      tests.forEach((message,test) {
        try {
          // Set Up
          if (_setUp != null) _setUp();

          // Run
          test();
          print('${message} : Green!');
        } catch (ExpectException e) {
          // Failure
          print('${message} : Red!');
          print('  ${e}');
        } catch (Exception e) {
          // Error
          print('${message} : Error!');
          print('  ${e}');
        }
      });
    })();
}

setUpが指定されない場合もあるでしょうから、nullチェックを入れています。

 

最後にtearDownを呼び出す、テストメソッドが失敗してもtearDownを呼び出す

tearDownもsetUpと同様に設定できるようにしましょう。

Test
main() {
  var runner = new TestRunner();

  runner.setUp = () => { /* set up */ };
  runner.tearDown = () => { /* tear down */ };

  var expect = '1';
  var actual = '2';
  runner.add('test description', () => Expect.equals(expect, actual));

  runner.run();
}

 

setUpと同様setterのみのプロパティとします。そして、tearDownは最後に呼び出し、なおかつ失敗しても呼び出さないといけない。ということは、finallyの中に書いてやるのが妥当でしょう。

TestRunner
class TestRunner {
  // Test Cases
  Map<String, Function> tests;

  // Constructor
  TestRunner() {
    tests = new LinkedHashMap<String, Function>();
  }

  // Add Test Case
  add(message, test) =>
    tests[message] = test;

  // Set Up
  Function _setUp;
  set setUp(Function func) => _setUp = func;

  // Tear Down
  Function _tearDown;
  set tearDown(Function func) => _tearDown = func;

  // Run Test Cases
  run() =>
    ((){
      tests.forEach((message,test) {
        try {
          // Set Up
          if (_setUp != null) _setUp();

          // Run
          test();
          print('${message} : Green!');

        } catch (ExpectException e) {
          // Failure
          print('${message} : Red!');
          print('  ${e}');
        } catch (Exception e) {
          // Error
          print('${message} : Error!');
          print('  ${e}');
        } finally {
          // Tear down
          if (_tearDown != null) _tearDown();
        }
      });
    })();
}

setUpと同様にnullチェックを忘れずに。

 

収集した結果を報告する

いよいよ大詰めです。テストの実行結果のサマリーを、テスト実行の後に出力してやりましょう。

これは単純に、テスト件数、失敗件数、エラー件数のカウンタを用意してやって、最後に出力するだけです。

TestRunner
class TestRunner {
  // Test Cases
  Map<String, Function> tests;

  // Constructor
  TestRunner() {
    tests = new LinkedHashMap<String, Function>();
  }

  // Add Test Case
  add(message, test) =>
    tests[message] = test;

  // Set Up
  Function _setUp;
  set setUp(Function func) => _setUp = func;

  // Tear Down
  Function _tearDown;
  set tearDown(Function func) => _tearDown = func;

  // Run Test Cases
  run() =>
    ((){
      var cnt = 0;
      var failures = 0;
      var errors = 0;

      tests.forEach((message,test) {

        cnt++;

        try {
          // Set Up
          if (_setUp != null) _setUp();

          // Run
          test();
          print('${message} : Green!');

        } catch (ExpectException e) {
          // Failure
          print('${message} : Red!');
          print('  ${e}');

          failures++;

        } catch (Exception e) {
          // Error
          print('${message} : Error!');
          print('  ${e}');

          errors++;

        } finally {
          // Tear down
          if (_tearDown != null) _tearDown();
        }
      });

      // Print results
      print('Tests run: ${cnt}, Failures: ${failures}, Errors: ${errors}');

    })();
}

これで完成です。私はこのコードに「DartUnit」と名付けGitHubで公開しています。

masaru-b-cl/DartUnit – GitHub

 

実際に使ってみよう

こんな感じで作成した「DartUnit」を実際に使ってみましょう。お約束のFizzBuzzです。

実際のコードはここで見れます。

http://try-dart-lang.appspot.com/s/LQkh

 

まとめ

いかがだったでしょうか。簡易なものであれば、案外簡単にテストランナーを作れることが分かってもらえたんじゃないかと思います。

昨今はメジャーなところからマイナーなところまで、あらゆる言語、プラットフォームに対するテスト環境が整っていますので、自分でテストランナーを自作する意義を見出せないかもしれません。

しかし、テストランナーを自作するということは、対象言語への理解を深めるためにもとても良いものです。

原典にもこうあります。

p.117 第24章 xUnitの回顧

・調査―筆者は新しいプログラミング言語に直面すると、xUnitを実装する。最初の8個から10個のテストを動作させる頃には、日々のプログラミングで使用する機能の多くの調査が済んでいる。

是非皆さんも、一度好きな言語でテストランナーを作ってみてはいかがでしょうか?

 

明日は?

@mao_instantlifeさんのTDDやってみてコメントが減った話です。

DartUnitの修正

YoutubeにアップしていたDartUnitをつかったFizzBuzzのデモ動画(http://www.youtube.com/watch?v=-f6bj-Z59h0)に、以下のようなコメントがついていました。

I tried the code at try-dart-lang, but it doesn’t run any more. Maybe Dart has changed. It gives an error on line 1 ‘Generative constructors cannot return arbitrary expressions’

ACobaltBomb

どうやら、Dartの仕様変更で、コンストラクタの定義にラムダ式を使えなくなったようです。

というわけで、コンストラクタを次のように直しました。

 

  // Constructor
  TestRunner() {
    _setUp = null;
    _tearDown = null;
    tests = new LinkedHashMap();
  }

 

修正したコードでのTry Dart Langサイトはこちら↓

http://try-dart-lang.appspot.com/s/LQkh

 

DartUnitの最新版はGitHubからどうぞ。

https://github.com/masaru-b-cl/DartUnit

 

しかし、Publicなところに公開することで、全世界の人からコメントがもらえるようになったとは、よい時代になったものです。