ホソカワです。
SmalltalkからJavaへの変換は、途中でしぼんでしまい、本家のMLでJava版もでてし
まいました。ただ、Jeffries氏になんらかの返事をしたいと思い、(本に名前を入れ
て頂いたし、)Extreme Programming Installed(I本?)のChapter 14 Test First,
by Intention のタスクを自分でプログラムしてみました。
実は、この話には、おちがあります。プログラムを読みたくない方は、ここで、「こ
こまで」をサーチして、そこからお読み下さい。
---- ここから
まず、Sum クラスを定義します。(I本のまねをして時間を入れてみました。)
--- Sum 12:20
public class Sum {
private String name;
private int amount;
public Sum(String name, int amount) {
this.name = name;
this.amount = amount;
}
}
二つのリストをマージする機能をもったSummarizerオブジェクトが必要です。このオ
ブジェクトを生成し、getSummary()で結果リストを取得します。早速、テストを書き
ます。
--- SummarizerTest 12:28
import junit.framework.*;
import java.util.*;
public class SummarizerTest extends TestCase {
public SummarizerTest(String name) {
super(name);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(SummarizerTest.class);
}
public void testEmpty() {
Summarizer emptySummarizer = new Summarizer(null, null);
assertNull(emptySummarizer.getSummary());
}
これをコンパイルするとSummarizerがないといわれるので、Summarizerを定義しまし
た。getSummary()は、単純にnullをかえすようにしました。I本は、firstとsecond
をくっつけていましたが、Java で簡単にできなかったので…
--- Summarizer 12:36
import java.util.*;
public class Summarizer {
private ArrayList first;
private ArrayList second;
public Summarizer(ArrayList first, ArrayList second) {
this.first = first;
this.second = second;
}
public ArrayList getSummary() {
return null;
}
}
これは、コンパイルし、見事テストもパスしました。でも、正しいコードが書かれて
いない事をメモしておきます。さて、本題のtestABC()を書く事にします。二つのリ
ストをあたえて、結果リストが思い通りのものかチェックしました。
--- SummarizerTest 12:57
public void testABC() {
ArrayList acCollection;
ArrayList abCollection;
ArrayList abcCollection;
Summarizer abcSummarizer = new Summarizer(acCollection, abCollection);
assertEquals(abcCollection, abcSummarizer.getSummary);
}
このSummarizerTestをコンパイルすると初期化されていないacCollection…が使用さ
れているとおこられます。その通りです。ここで、acCollectionなど初期化するので
すが、コードをここに書くと読みにくくなるので、別クラスでacCollectionを作るよ
うにしました。それが、SummarizerTestHelperクラスです。
--- SummarizerTestHelper 13:10
import java.util.*;
public class SummarizerTestHelper {
public ArrayList getACCollection() {
ArrayList collection = new ArrayList();
collection.add(new Sum("A", 1));
collection.add(new Sum("C", 2));
return collection;
}
public ArrayList getABCollection() {
ArrayList collection = new ArrayList();
collection.add(new Sum("A", 10));
collection.add(new Sum("B", 3));
return collection;
}
public ArrayList getABCCollection() {
ArrayList collection = new ArrayList();
collection.add(new Sum("A", 11));
collection.add(new Sum("C", 2));
collection.add(new Sum("B", 3));
return collection;
}
}
SummarizerTestHelperは、テストに必要なメソッドを集めたクラスです。これを使っ
て、SummarizerTestに書き加えました。
--- SummarizerTest 13:20
private SummarizerTestHelper helper;
public void testABC() {
ArrayList acCollection = helper.getACCollection();
ArrayList abCollection = helper.getABCollection();
ArrayList abcCollection = helper.getABCCollection();
Summarizer abcSummarizer = new Summarizer(acCollection, abCollection);
assertEquals(abcCollection, abcSummarizer.getSummary());
}
protected void setUp() {
helper = new SummarizerTestHelper();
}
コンパイルは通りましたが、テストでfailします。そろそろ中身を書かないと…
--- Summarizer 13:32
public ArrayList getSummary() {
ArrayList summary = new ArrayList();
summarize(summary, first);
summarize(summary, second);
return summary;
}
ここは、I本を参考にして、リストをひとつづつ処理するメソッドsummarizeを作りま
した。もちろんコンパイルしないので、summarizeを書きました。
--- Summarizer 13:43
private void summarize(ArrayList summary, ArrayList collection) {
Iterator it = collection.iterator();
while (it.hasNext()) {
Sum sum = (Sum) it.next();
Sum foundSum = findSum(summary, sum);
if (foundSum == null) {
summary.add(sum);
} else {
foundSum.setAmount(foundSum.getAmount() + sum.getAmount());
}
}
}
ここで、結果リストに同じ名前のSumがある場合は、amountの合計をすることと、無
い場合は、アペンドする機能を書きました。今度は、findSumがありません。それか
ら、setAmountも…
--- Summarizer 13:50
private Sum findSum(ArrayList summary, Sum sum) {
Iterator it = summary.iterator();
while (it.hasNext()) {
Sum summarySum = (Sum) it.next();
if (summarySum.getName().equals(sum.getName())) {
return summarySum;
}
}
return null;
}
--- Sum 13:53
public Sum(String name, int amount) {
this.name = name;
setAmount(amount);
}
public String getName() {
return name;
}
public int getAmount() {
return amount;
}
public void setAmount() {
this.amount = amount;
}
これで、コンパイルするようになりました。ちょっとリファクタリングもしました。
Sumのコンストラクタで、this.amountに直接代入していたところをsetAmount()に変
えました。Once And Only Once ルールです。
さて、やっと、テストが実行出来ます。ところが、実行してみるとtestEmptyテスト
がnull pointer exception で、また、testABC もfailします。まず、testEmptyを追っ
てみました。これは、簡単で、summarizeで、null リストからiteratorを取得すると
ころで落ちていました。null チェックするよりは、使い方の問題かなと思い、テス
トの方を変更しました。
--- SummarizerTest 13:57
public void testEmpty() {
Summarizer emptySummarizer =
new Summarizer(new ArrayList(), new ArrayList());
assert(emptySummarizer.getSummary().isEmpty());
}
これで、testEmptyがパスするようになりました。
testABCの方は、まず、テストが間違っていたのでそれをなおす事にしました。
assertEqualsは、オブジェクトが等しいかどうかチェックするだけで、リストの中身
が等しいかチェックをしていません。
--- SummarizerTest 14:01
public void testABC() {
ArrayList acCollection = helper.getACCollection();
ArrayList abCollection = helper.getABCollection();
ArrayList abcCollection = helper.getABCCollection();
Summarizer abcSummarizer = new Summarizer(acCollection, abCollection);
assertEquals(helper.isEqual(abcCollection, abcSummarizer.getSummary()));
}
新しいメソッドhelper.isEqual()がでてきましたので、それを書きます。
--- SummarizerTestHelper 14:13
public boolean isEqual(ArrayList first, ArrayList second) {
if (first.size() == second.size()) {
for (int index = 0; index < first.size(); index++) {
if (! first.get(index).equals(second.get(index))) {
return false;
}
}
}
return false;
}
--- Sum
/* test only */ public boolean equals(Sum sum) {
return name.equals(sum.getName()) && amount == sum.getAmount();
}
コンパイル、そしてテストしましたが、failします。何が悪いのか良く分かりません。
残念でしたが、print文を入れる事にしました。
--- SummarizerTestHelper 14:18
public boolean isEqual(ArrayList first, ArrayList second) {
if (first.size() == second.size()) {
for (int index = 0; index < first.size(); index++) {
System.out.println("first " + first.get(index).toString());
System.out.println("second " + second.get(index).toString());
if (! first.get(index).equals(second.get(index))) {
return false;
}
}
}
return false;
}
--- Sum
/* test only */ public String toString() {
return name + " " + amount;
}
これを実行すると、表示はこうでした。
A 11
A 11
一つ目のSumを比較するところでreturnしているようです。if文の条件に問題がある
と思い、Sumのequals()を見直しましたが、間違っていません。equals()は、オーバー
ライドして、定義しているんだよねと考えていたら、ふと気付きました。Sum
のequalsが呼ばれていないのではないか?そこで、(またですが)print文を入れま
した。
--- Sum 14:48
/* test only */ public boolean equals(Sum sum) {
System.out.println("Sum.equals");
return name.equals(sum.getName()) && amount == sum.getAmount();
}
これで、テストを実行しましたが、思っていた通り、「Sum.equals」は、表示されま
せんでした。そこでキャストを使い、isEqualを修正しました。
--- SummarizerTestHelper 14:51
public boolean isEqual(ArrayList first, ArrayList second) {
if (first.size() == second.size()) {
for (int index = 0; index < first.size(); index++) {
System.out.println("first " + first.get(index).toString());
System.out.println("second " + second.get(index).toString());
if (! ((Sum) first.get(index)).equals((Sum) second.get(index)))
{
return false;
}
}
}
return false;
}
直りました。テストがパスしました。
---- ここまで
結局、Test First Programming は、凄いというつもりが言えなくなってしまいまし
た。print文に頼らざるえなくなったのは、以下の理由からかと思っています。
1.乗ってくるとテストを書かなくなる。20分ぐらい、テストなしでコードをかいてい
ますね。これは、ペアプログラミングをしていれば、相方が注意してくれたでしょう
か?
2.コンパイルを通すために芋づる的にどんどんコードだけを書いてしまうような気が
します。中身のないメソッド、例えば、return null だけのメソッド、を書いてコン
パイルを通すような事をもっと行うべきか?
3.メソッドがprivateだったため、テストを怠ったところがありました。Summarizer
のsummarizeとfindSumは、テストが必要だったでしょう。
4.それから、今回、問題の根源であったSummarizerTestHelperのisEqualは、public
では、ありましたが、テストのヘルパーという位置付けのため、テストをする事を全
く考えていませんでした。ヘルパーとは、どのようなものか考えなくてはいけない。
まあ、プログラムを書いて、勉強になりました。Test First Programming でも、デ
バッグは必要ですね。
--
Kaoru Hosokawa
khosokawa@....com