この記事は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で作ってますが、私はめんどくさくてスルーしました(えー
テストメソッドを呼び出す、複数のテストを実行する
まず、大まかに考えましょう。テストメソッドを呼び出す手順は、だいたい次のようになるでしょう。
- テストランナーのオブジェクトを作る
- 実行したいテストを登録する
- テストを実行する
テストランナーのオブジェクトを作る
テストを実行するには、まずは次のようにテストランナーのオブジェクトを作るでしょう。
var runner = new TestRunner();
}
これはもう単純にクラスを定義するだけです。
}
実行したいテストを登録する
次にテストの登録です。原典ではPythonを使って、「文字列」でテストの名前を登録して実行しています。しかし、Dart言語はラムダ式を用いて、関数をオブジェクトして簡単に扱える機能がありますので、これを使わない手はないでしょう。
具体的には次のような感じで書きたいですね。
var runner = new TestRunner();
var expect = '1';
var actual = '2';
runner.add('test description', () => Expect.equals(expect, actual));
}
Expectクラスは、JUnitでいうAssertクラスのようなもので、検証用のメソッドがいくつか用意されています。
このインターフェースに合わせてテストランナーを書くと、次のようになります。
// Test Cases
Map<String, Function> tests;
// Constructor
TestRunner() {
tests = new LinkedHashMap<String, Function>();
}
// Add Test Case
add(message, test) =>
tests[message] = test;
}
LinkedHashMapを使っているのは、登録したテストケースの順序を保つためです。
テストを実行する
最後に登録したテストを実行させます。次のような感じで実行したいですね。
var runner = new TestRunner();
var expect = '1';
var actual = '2';
runner.add('test description', () => Expect.equals(expect, actual));
runner.run();
}
今回は簡易的なものなので、テストの実行結果は標準出力に出しちゃいましょう。ということで、テストランナーは次のようになります。
// 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もテストケースと同様に、ラムダ式で渡せれば素敵ですよね。
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メソッド内の最初で呼び出すようにしましょう。
// 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と同様に設定できるようにしましょう。
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の中に書いてやるのが妥当でしょう。
// 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チェックを忘れずに。
収集した結果を報告する
いよいよ大詰めです。テストの実行結果のサマリーを、テスト実行の後に出力してやりましょう。
これは単純に、テスト件数、失敗件数、エラー件数のカウンタを用意してやって、最後に出力するだけです。
// 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で公開しています。
実際に使ってみよう
こんな感じで作成した「DartUnit」を実際に使ってみましょう。お約束のFizzBuzzです。
実際のコードはここで見れます。
http://try-dart-lang.appspot.com/s/LQkh
まとめ
いかがだったでしょうか。簡易なものであれば、案外簡単にテストランナーを作れることが分かってもらえたんじゃないかと思います。
昨今はメジャーなところからマイナーなところまで、あらゆる言語、プラットフォームに対するテスト環境が整っていますので、自分でテストランナーを自作する意義を見出せないかもしれません。
しかし、テストランナーを自作するということは、対象言語への理解を深めるためにもとても良いものです。
原典にもこうあります。
p.117 第24章 xUnitの回顧
・調査―筆者は新しいプログラミング言語に直面すると、xUnitを実装する。最初の8個から10個のテストを動作させる頃には、日々のプログラミングで使用する機能の多くの調査が済んでいる。
是非皆さんも、一度好きな言語でテストランナーを作ってみてはいかがでしょうか?