| January, 2000 |
| ユニットテストをする人々は、Extream Programmingでのユニット テストをする人たちの多くさえも、ユーザインターフェイスのテストは必要ないと考えている。 しかし、それはできるのだ。JUnitをこの種のテストに使うことができるのである。 この論文では、小さいが納得のいく例を通してJUnitを使ったテストとプログラミングの雰囲気 を示そうと思う。この論文は第二部だが、それだけでも読むことができる。 第一部ではモデルを開発 している。 (原論文はThe Test/Code Cycle in XP: Part 1, Model および The Test/Code Cycle in XP: Part 2, GUI を参照。 この原論文は、"Extreme Programming Explored" by William C. Wake, Addison Wesley 2001 に収録されています。日本語版は「XPエクストリーム・プログラミング アドベンチャー」長瀬 嘉秀 (翻訳), 今野 睦 (翻訳), 飯塚 麻理香 (翻訳), 畑田 成広 (翻訳) 発行 ピアソン・エデュケーション 2002年です) |
開発はXPのスタイルですすめる。つまり、テストとコーディングの間をいったりきたり
するわけだ。
以下でコードの引用をするときはこれを反映して、テストコードは左側に寄せ、
アプリケーションコードは右側に寄せて表示する
|
GUIのテストと開発では、モデルの interfaces に依存するのはかまわない。 具象クラスに依存しなくてはならなくなると幸せは減ってしまうからね。
というわけで、testWidgetsPresent()を作る。 これを動くようにするために、全体のスクリーンに対するpanel("SearchPanel")と ラベル("searchLabel")とqueryのためのテキストフィールド("queryField")、 ボタン("findButton")、それから結果を表示するためのテーブル("resultTable") が必要である。 これらのwidgetはパッケージ内でアクセスできるようにし、テストがそれを 見えるようにする。
public void testWidgetsPresent() {
SearchPanel panel = new SearchPanel();
assertNotNull(panel.searchLabel);
assertNotNull(panel.queryField);
assertNotNull(panel.findButton);
assertNotNull(panel.resultTable);
}
|
テストのコンパイルは失敗する。(勿論だ。まだSearchPanelは作っていないのだから) そこで、SearchPanelクラスを作る。それにはフィールドのwidgetを持たせ、コンパイル できるようにする。 まだwidgetをinitializeしてはいけない。その前に、テストを走らせ、それが失敗することを 確認するのだ。(テストが一回は失敗するのを確認しておくのはいい習慣だ。 これで間違っているときにテストがそれを判定できるということの確認になる。 それによって、テストがコーディングをドライブしていることの保証がえられる) 次に、テストが通るのに必要なだけのコードを書く。
注意すべき点:
他のテストとして、widgetが正しくセットアップされたかどうかを検証することもできる。
public void testInitialContents() {
SearchPanel sp = new SearchPanel();
assertEquals("Search:", sp.searchLabel.getText());
assertEquals("", sp.queryField.getText());
assertEquals("Find", sp.findButton.getText());
assert("Table starts empty", sp.resultTable.getRowCount() == 0);
}
|
テストを走らせ、うまく行くのを確認する。
この段階で、SearchPanelのコードはこんな感じになっている:
|
ここで二つの道のどちらに進んでもよい。つまり、ユーザインターフェイス の開発か、検索機能との連携か。 インターフェイスを目に見たいという衝動は強いが、それに逆らって連携機能の ほうに軍配をあげることにしよう。
パネルには二つのメソッドgetSearcher() と setSearcher()を作る。 これはSearcherとパネルを結び付けるものだ。 この決断の結果、新たに次のテストを書くことになる。
public void testSearcherSetup() {
Searcher s = new Searcher() {
public Result search(Query q) {return null;}
};
SearchPanel panel = new SearchPanel();
assert ("Searcher not set", panel.getSearcher() != s);
panel.setSearcher(s);
assert("Searcher now set", panel.getSearcher() == s);
}
|
コンパイルは失敗し、SearchPanelを修正することになり、メソッドを追加し、 テストをもう一度実行し、また失敗する。そこで、set/getメソッドを作り、 そしてテストをパスする。
このバネルはまだほとんどなにもできないが、いまや、Searcherとそれを つなぐことができたのである。
これはユニットテストなので、実際のSearcherの実装に依存したテストは したくない。テスト専用のものを作っておきたい。 こうすれば、粒度のそろった方法で、振舞いをコントロールすることができるように なる。 ここでは、TestSearcherという名前の新しいSearcherを作る。 query文字列は一つのintegerとし、それが検索結果の個数を示す。 itemには、最初の著者に"a0", 二番目の標題に"t1"、という具合に名前をつけていく。
しかし、最初は....テストだ。(これは、GUIのテストでなく、テストをするクラスの テストだということをお忘れなく)
public void testTestSearcher() {
assertEquals(new Query("1").getValue(), "1");
Document d = new TestDocument(1);
assertEquals("y1", d.getYear());
Result tr = new TestResult(2);
assert(tr.getCount() == 2);
assertEquals("a0", tr.getItem(0).getAuthor());
TestSearcher ts = new TestSearcher();
tr = ts.find(ts.makeQuery("2"));
assert("Result has 2 items", tr.getCount() == 2);
assertEquals("y1", tr.getItem(1).getYear());
}
|
普通のコンパイル/失敗のサイクルを行い、テストクラスを作る。 TestDocumentから始めよう:
public class TestDocument implements Document {
int count;
public TestDocument(int n) {count = n;}
public String getAuthor() {return "a" + count;}
public String getTitle() {return "t" + count;}
public String getYear() {return "y" + count;}
}
|
TestResultクラスはintegerをパラメタにとるコンストラクタを持つ。 そのintegerで何件の結果を返すかを指定する。
public class TestResult implements Result {
int count;
public TestResult(int n) {count = n;}
public int getCount() {return count;}
public Document getItem(int i) {return new TestDocument(i);}
}
|
TestSearcherはquery文字列の数値としての値を用いてresultを作る。
public class TestSearcher implements Searcher {
public Result find(Query q) {
int count = 0;
try {count = Integer.parseInt(q.getValue());}
catch (Exception ignored) {}
return new TestResult(count);
}
}
|
テストをまた実行し、今度はパスする。
public void test0() {
SearchPanel sp = new SearchPanel();
sp.setSearcher (new TestSearcher());
sp.queryField.setText("0");
sp.findButton.doClick();
assert("Empty result", sp.resultTable.getRowCount() == 0);
}
|
ついに、GUIを使えるようになった。 つまり、テキストフィールドに設定し、ボタンを押し、といったことができる。
テストを走らせ--そしてパスした! これは、今や我々は機能する解を持っているということを意味する。 ただし、使っているsearcherが常に0itemを返す場合だけだが。
前進しよう:
public void test1() {
SearchPanel sp = new SearchPanel();
sp.setSearcher (new TestSearcher());
sp.queryField.setText("1");
sp.findButton.doClick();
assert("1-row result", sp.resultTable.getRowCount() == 1);
assertEquals(
"a0",
sp.resultTable.getValueAt(0,0).toString());
}
|
ここで、失敗する。
理由は、ボタンに対するイベントハンドリングをまだつくっていないからだ。
ボタンがクリックされたら、テキストフィールドの文字列からqueryを生成し
それからsearcherにそれを検索させ、その結果をテーブルに表示するということを
したい。
ところが、型が合わないのだ: SearcherはResultを返してくるが、GUIのテーブルは
TableModelを要求している。
そのため、インターフェイスの変換を行うアダプターが必要になる。
|
コンパイルに失敗したら、ダミーの実装でスタブを作る:
|
Test0()はここでも通る。そして test1()はまだ失敗する。
アダプタを書くのは特に難しいことはない。しかし、テストを書くことから始める。
public void testResultTableAdapter() {
Result result = new TestResult(2);
ResultTableAdapter rta = new ResultTableAdapter(result);
assertEquals("Author", rta.getColumnName(0));
assertEquals("Title", rta.getColumnName(1));
assertEquals("Year", rta.getColumnName(2));
assert("3 columns", rta.getColumnCount() == 3);
assert("Row count=2", rta.getRowCount() == 2);
assertEquals("a0", rta.getValueAt(0,0).toString());
assertEquals("y1", rta.getValueAt(1,2).toString());
}
|
テストは失敗する。理由は、ダミーの実装では何もしていないからだ。
戻って、ResultTableAdapterの実装をする。 (DefaultTableModelから)AbstractTableModelのサブクラスに変更する。 そして、column namesとcolumn、row countsそしてついに getValueAt() を実装する
|
このテスト(testResultTableAdapter) は通るはずで、
test1()も通らないといけない。
他になにか問題になりそうなことがあるだろうか? ひとつ起りそうな問題は、queryを続けて実行したとき、"leftover"になるか どうかというのがある。つまり、結果が5件のqueryの後に結果が3件の queryをすると、テーブルには3件だけがなくてはいけない。(もしテーブルの 作り方が間違っていたら、先に実行した結果の末尾に2件が残って見える かもしれない。)
連続するqueryのテストもできる。
public void testQuerySequenceForLeftovers() {
SearchPanel sp = new SearchPanel();
sp.setSearcher (new TestSearcher());
sp.queryField.setText("5");
sp.findButton.doClick();
assert(sp.resultTable.getRowCount() == 5);
sp.queryField.setText("3");
sp.findButton.doClick();
assert(sp.resultTable.getRowCount() == 3);
}
|
このテストはとおる。
このテストを走らせることができるためには、作ってきたpanelをframeか windowの上にのせる必要がある。 (Componentはそれを含むwindowが作られるまでは画面上に配置されない)
public void testRelativePosition() {
SearchPanel sp = new SearchPanel();
JFrame display = new JFrame("test");
display.getContentPane().add(sp);
display.setSize(500,500);
display.setVisible(true);
//try {Thread.sleep(3000);} catch (Exception ex) {}
assert ("label left-of query",
sp.searchLabel.getLocationOnScreen().x
< sp.queryField.getLocationOnScreen().x);
assert ("query left-of button",
sp.queryField.getLocationOnScreen().x
< sp.findButton.getLocationOnScreen().x);
assert ("query above table",
sp.queryField.getLocationOnScreen().y
< sp.resultTable.getLocationOnScreen().y);
}
|
テストは失敗する。まだ、widgetをpanelの上に乗せていないから。 (sleep()のコメントをはずせば、スクリーン上で画面を見ることもできる)
panelを実装するために、私は普通、中間のパネルとレイアウトが見えるように スクリーンをデザインする:
さて、このパネルをレイアウトできるようになった:
|
コンパイルし、テストし、ちゃんと動く。
パネルがうまく作れた!
|
[Written 1-3-2000; revised 2-1-2000; re-titled and revised 2-4-2000.]
Copyright 2000, William C. Wake ....... Email ....... サイトマップ ....... サイトのトップ
翻訳)大村伸一 メール...この翻訳は Addison Wesley の許可を得ています。営利目的での使用は禁止しします。