
eXtreme Programming がしたい!
|
eXtremeで行こう!-First eXtreme Iteration
導入も「テストファースト」で
この時点で、XPに対するチームの中での認知度はあまり高いものではありませんでした。開発チームでは、山本と篠原が、XPについての書籍やネット上の文献などを調べていたり、早川がJUnit(注3) を使ってみたりはしていたものの、水谷を含めた残りのメンバーは、「XP? 聞いたことあるけど……ペアでコード書くっていうあれでしょ? どんなメリットがあるの? 開発スピードが遅くなるだけじゃないの?」と、やや懐疑的な雰囲気だったのです。XPをそれなりに知っていたメンバーにしたところで、実効性について確信があったわけではありません。
ただ、テスティングとリファクタリングについては、このプロジェクト以前に社内で評価を行った結果、間違いなく有用であるとわかっていました。実プロジェクトに適用した場合、テストケースを書く時間が開発速度に悪影響を与えるんじゃないか、という危惧があったのは確かです。しかし、これ以降の開発では、ビジネスの要請によって試行錯誤的に機能追加を進めていくことになり、随時リグレッションテストを行う必要があることがわかっています。そのためには、いずれにしてもテストの自動化は必須でした。
また、一般的なプロトタイプのコードよりは品質的にマシとはいうものの、製品品質のコードであるとまでは云えません。機能単位で分業を行った結果、重複コードは各人が作ってしまっているし、綿密なモデリングによったクラス抽出ではなかったために、責務の偏りなどが随所に見られるものでした。全面的な書き直しを必要とするものではないが、手を入れないで走りつづけるわけにもいきません。リファクタリングも必須というべき状況でした。
そこで、顧客と水谷が次期リリース計画のベースとなるユースケースを作っている期間を利用して、開発チームではまずテストケースを実装することからはじめました。いわゆるテストファーストでは、テストを書くことでクラスの外部仕様を明らかにし、それから実コードを書くことになります。テストが仕様書がわりになるわけですが、それからすると、リバースエンジニアリングしたようなものです。
この時点ではまだXPのフルスペックを適用するつもりはありませんでしたが、JUnitを使用したことがあるメンバーも少なかったため、ペアでテストケースを実装することにしました。この時期は、実装はまるで行わずに、リファクタリングの帽子(注4)をかぶりつづけることになります。 山本と早川でペアを組み、早川がドライバーとなってこのプロジェクトのためにはじめて書いたテストケースがリスト1です。
リスト1
package net.lex.entity;
import java.io.*;
import junit.framework.*;
public class PatternFilenameFilterTestCase extends TestCase{
private PatternFilenameFilter filter;
public PatternFilenameFilterTestCase(String name){
super(name);
filter = new PatternFilenameFilter();
}
public void testAccept(){
filter.setPattern("ax");
assertTrue(filter.accept(
new File("lib"),"jaxp.jar"));
filter.setPattern("zzz");
assertEquals(false, filter.accept(
new File("lib"),"jaxp.jar"));
}
}
これはjava.io.FilenameFilterインターフェースを実装するクラス、PatternFilenameFilterクラスに対するテストケースです。配信するメールやWebページを生成するために、テンプレートファイルをiMode用/JSky用/ezWeb用、と区分けして用意しており、それらのテンプレートファイルをリストアップするのに使われるものです。
/var/template/t1_ic.txt ……… iMode カラー用 t1_ib.txt ……… iMode 白黒用 t1_jb.txt ……… JSky カラー用 t1_jb.txt ……… JSky 白黒用 t1_eb.txt ……… ezWeb カラー用 t1_eb.txt ……… ezWeb 白黒用
といった構成でテンプレートファイルが用意されているとき、
リスト2
// テンプレートファイルのディレクトリ
File templateDir = new File("/var");
// テンプレート番号 1番のファイルは
// ファイル名に "t1_" を含む
PatternFilenameFilter templateFilter =
new PatternFilenameFilter();
templateFilter.setPattern("t1_");
// テンプレートファイルのリストを取得
File[] templateFiles =
templateDir.listFiles(templateFilter);
// テンプレートから実際の配信ファイルを生成
for(int i = 0; i < templateFiles.length; i++) {
createDeliveryFile(templateFiles[0]);
}
のように利用します。
PatternFilenameFilterクラスはaccept(File,String)メソッドを実装します。accept(File,String)メソッドは、第二引数で与えられたファイル名にsetPattern(String)メソッドで設定された文字列を含めば、第一引数の親ディレクトリがなんであれtrueを返すという仕様になっています(リスト2)。
リスト3
package net.lex.entity;
import java.io.*;
class PatternFilenameFilter
implements FilenameFilter {
private String pattern;
public boolean accept(
File dir, String filename) {
return (filename.indexOf(this.pattern) >= 0);
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
}
このテストケースは、その仕様をテストして、本体コードにもバグがなかったので問題なく通ります。

しかし、これで充分というわけではありません。このテストケースの場合、PatternFilenameFilterの仕様を充分に満たしているとは言えません。
早川:「通りましたね。これで完成としていいですか?」
山本:「いや、それじゃまだまだ足りないね。いま書いたのは明示的な仕様で、それはちゃんとテストできてると思うよ。でも実は暗黙的な仕様ってものがあるんだ」
早川:「暗黙的な仕様ですか?」
山本:「そう。たとえば、ファイル名の大文字小文字を区別するのかどうかについてはテストしてないよね。他にも、引数にnullが与えられたときの動作だとか、setPattern()されていない状態での動作についてはどうだろう」
早川:「あぁ……テストしてないです」
山本:「今回はそもそもメソッドごとに仕様書があるわけじゃないけど、大文字小文字の区別なんかは、ちゃんと仕様として定義しておかなくちゃいけないよね。他にも、文字列の比較をするなら、trim()してから比較するかどうかなんかも必ず出てくる話だよ」
早川:「言われてみればそうですよね」
山本:「これまでは使い捨てにするかもしれないソースコードだし、外にライブラリとして提供するわけでもないから、曖昧にしておいても支障はなかっただけなんだよ。せっかくいま仕様書としてテストを書いてるんだから、そこもちゃんとテストしておこう」
文字列比較をするときの注意点や、nullチェックは当然どこかでしなくてはならないなどのことは、経験があればわかるでしょう。しかし、たいていの仕様書が不足しているプロジェクトでは、こういった「そこそこの経験者なら経験的にわかっているだろうと思われること」が不明確なままです。しかも「わかっているだろうと思われること」は、経験者同士でも人それぞれです。そのため、nullチェックであれば呼び出し側とメソッド側の両方でチェックしてしまう冗長なコードや、どちらでもチェックしないで実行時エラーになってしまう脆弱なソフトウエアを産んでしまいます。ペアプロでテストケースを書く場合であっても、双方が初心者であると、こうした危険は避けられません。
ここでは、patternとファイル名において大文字小文字は区別して扱うこと、setPattern()されていない状態でのaccept()呼び出しはNullPointerExceptionをthrowすること、他のメソッドにおいても引数のnullチェックは呼び出し元の責務であること、という現在の仕様を明らかにするよう、テストケースを改良してみましょう。
リスト4
public void testAccept()
throws Exception { //(1)
// setPattern()されていない時にaccept()を
// 呼び出すとNullPointerException。
try { //(2)
filter.accept(
new File("lib"),"jaxp.jar");
fail();
} catch(NullPointerException ex) {
}
filter.setPattern("ax");
assertTrue(filter.accept(
new File("lib"),"jaxp.jar"));
// 大文字小文字は区別する。
assertEquals(false, filter.accept(
new File("lib"),"JAXP.jar")); //(3)
// accept()を呼び出す際、引数がnullであれば
// NullPointerException。
try { //(4)
filter.accept(null,"jaxp.jar")};
fail();
} catch(NullPointerException ex) {
}
try { //(5)
filter.accept(new File("lib"),null)};
fail();
} catch(NullPointerException ex) {
}
filter.setPattern(null);
try { //(6)
filter.accept(
new File("lib"),”jaxp.jar”)};
fail();
} catch(NullPointerException ex) {
}
filter.setPattern("zzz");
assertEquals(false, filter.accept(
new File("lib"),"jaxp.jar"));
}
testAccept()はリスト3のようになりました。まずシグネチャが変わっており、throws Exception が宣言されています。これはテストケースを書くときのコツで、投げられるべき例外についてはテストケース内でcatchしテスト成功とすること、投げられるべき例外が投げられなかったときは明示的に失敗(fail())することをルールにします。投げられるべきでない例外が投げられたときは、テストの中でcatchしなければ、自動的にJUnitフレームワークがcatchしてそのテストが失敗であったことを明確にしてくれます。
そのようにして追加されたテストが(2)、(4) 、(5) 、(6)です。例外を投げるべきメソッド呼び出しをtry~catchで囲み、catchでは投げられるべき例外だけを明示的に指定します。例外がcatchされなかった場合はテストは失敗です(fail())。
また、(3)では大文字小文字を区別することを確認するテストを追加しています。テストケースには、できるだけテストの意図を明らかにするコメントを書くようにしました。わたしたちのプロジェクトでは、この時期以降、クラス本体にはほとんどコメントを書くことをしませんでした。その代わりにこのようなテストケースを書くことで、クラスのテストケースを見れば、サンプルコードとともにメソッドの外部仕様が明らかになるようにしてあります。テストケースをドキュメントとして使っているのです。
一般的なドキュメントはもちろん、JavaDocのコメントとしてソースファイルと同じところに管理したとしてさえ、工程が進んでいくにつれてソースコードの実態とドキュメントはかけ離れていくのが普通です。それを防ぐためには、まず不一致の個所を確実に補足するという膨大な作業が必要になります。
しかしテストケースであれば、実態と乖離したときはテストが失敗するというかたちで検出することが可能です。テストケース内のコードとコメントの乖離や、検査されるべき要件がテストされていない場合など、この手法でもすべてが解決するわけではありませんが、詳細ドキュメントのメンテナンスに頭を痛めている方は、このようなメリットにも目を向けてみてはいかがでしょうか(注5) 。
(注3)UXPの父のひとりKent BeckとデザインパターンのErich Gamma が作ったテスティングフレームワーク。オープンソースライセンスを採用し、フリーで提供されている。
http://www.junit.org/
(注4)リファクタリングを行う場合は、自分が今、なにか新しい実装をしているのか、リファクタリングを行っているのかを明確に意識する必要があります。比喩的にリファクタリングの帽子と実装の帽子をかぶりなおすと云います。
(注5)これで解決できるのはかなり詳細なクラス仕様書やメソッド仕様書レベルのドキュメントだけです。基本設計書などはこれまでどおりに管理する必要があります。それでも、ボリュームの少ない概要資料などに比べて、詳細資料は実態とかけ離れたものになりやすいものですから、ここだけでもメンテナンスの手間が軽減されるメリットはあると思います。まぁ、後付けのドキュメントを作っている場合はあまり気にする必要はないかもしれませんが。
