Index: [Article Count Order] [Thread]

Date:  Sat, 25 Nov 2000 20:51:08 +0900
From:  Kaoru Hosokawa <khosokawa@....com>
Subject:  [XP-jp:01184] Test First Programming 	でも、デバッグは必要ですね。
To:  extremeprogramming-jp@....jp (extremeprogramming-jp ML)
Message-Id:  <B645D684.2C15%khosokawa@....com>
Posted:  Sat, 25 Nov 2000 20:50:13 +0900
X-Mail-Count: 01184

ホソカワです。

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