J U nit テスト熱中症:プログラマは、テストを書くのが好きになる
原題: Test Infected: Programmers Love Writing Tests
テスト作業が、開発作業の中にしっかりと組み込まれていない。こうなる と、開発の進み具合を計測することは不可能になってしまう。というのも、あ るの機能が動き始めたのはいつか、またある機能が動かなくなったのはいつか、 まったくわからなくなるからだ。 JUnit を使えば、苦労なく、しかも段階的に、テストスィートを構築 できる。このテストスィートは、進捗状況を把握したり、意図しない副次効果 を見つけだしたり、また開発で労力をかけるべき箇所を明らかにしたりする上 で役に立つだろう。
目次
問題
どんなプログラマだって「コードを書いたらテストも書くべきだ」というこ とは知っている。にもかかわらず、実際に書いている人は少ない。「何で書か なかったの?」という質問に対しては、「あまりにも忙しすぎて」という同じ 答えが返ってくる。しかしこれは悪循環のはじまりだ -- プレッシャーを 感じればそれだけ、テストを書く量が減る。テストを書く量が減ればそれだけ、 生産性は落ち、コードの安定性も落ちる。あなたの生産性と正確さが悪くなれ ばそれだけ、さらなるプレッシャーを抱え込むことになるのだ。まさにこのサイクルの中でプログラマーは燃え尽きる。循環を断ち切るに は、外から影響を与えてやる必要がある。ぼくたちは、必要とされるこの「外 からの影響」が、単純なテストフレームワークの中にあることに気づいた。こ のテストフレームワークが提供するテスト作業は、少ない量ながら大きな違い を産み出すのだ。
自分でテストを書くことの大事さをあなたに納得してもらうためには、 一番よいのは、ぼくたちと共になんか小さな開発をしてもらうことだろう。 開発の中ではきっと、新たなバグが発生し、テストによってそれを見つけだし、ま た発生して、それを直して……といったことがおこなわれるだろう。自分の単 体テストを書いて、保存して、再実行することから得られる即座のフィードバッ クがどれだけ大事か、きっと納得してもらえるはずだ。
残念なことに、これはあくまで論文であって、外に広がる中世の通商路で のにぎわいと、階下のレコード店から漏れるテクノのリズムが聞こえる、そ んな魅惑的なチューリッヒの町を見下ろすオフィスではあったりはしないの で、開発の過程を「シミュレート」する他ないだろう。簡単なプログラムと そのテストを書いて、テストを走らせた結果を見せることにする。こうする ことで、実際にぼくたちが使用して、かつ支持しているプロセスについて、 感じをつかんでもらえるはずだ。これなら滞在費を払う必要もないし。
実例
ここからは、コードとテストの相互作用 (interplay)について注意を払いながら読んで欲しい。何行かのコードを書 いて、それが通るべきテストを書いて、もしくはさらに踏み込んで「通らない」 テストを書き、その後にテストが通るような修正コードを書く、といったスタ イルを採用することにする。ぼくたちが書くプログラムは、複数の通貨がからむ算術を表現する、とい う問題を扱うものである。同じ通貨内での算術は簡単で、単に足し引きする だけだ。数字を用いるだけで充分で、通貨単位の存在など忘れてしまっても かまわない。
しかし複数の通貨がからむと、俄然おもしろくなる。計算のために、単に 一つの通貨から別の通貨に交換すればよいというものではない。というのも、 固定の交換比率というものはないからだ -- 昨日の為替相場と今日の為替相場 でポートフォリオを組んで、その価値を比較してみる必要だってあるかもしれ ないのだし。
とはいえ、まずは簡単に、単一通貨での価格を表すMoney クラスを書くところからはじめよう。量は単純な int で表すことにする。と ことん正確にやろうとするなら、double とか、任意精度の符号付き十進数を 保存するための java.math.BigDecimal とかが必要とされるだろう。通貨を表す には、ISO で定められた三文字(USD, CHF, 等々)略語を表す String を用 いる。より複雑な実装では、「通貨」それ自体のオブジェクトを切ったほうが よいだろう。
class Money { private int fAmount; private String fCurrency; public Money(int amount, String currency) { fAmount= amount; fCurrency= currency; } public int amount() { return fAmount; } public String currency() { return fCurrency; } }二つの同じ通貨の Money を足したらどうなるか。 その二つの Money の持つ amount を足した値が、結果を表す Money の amount となる。
public Money add(Money m) { return new Money(amount()+m.amount(), currency()); }ではここで、さらにコードを書き進める前に、その場でのフィードバックを得 るために"コードを少し、テストを少し、コードを少し、テストを少し..."と いうプラクティスを実践したいと思う。テストの実装には JUnit のフレーム ワークを用いる。テストを書くには、最新版の JUnit が必要だ(もしくは自分で同等品を書いてもよい -- それほどの大仕事ではない)
JUnit では、テストケースの構成を決めるやり方が決まっており、さらに、 それらのテストを実行するためのツールも用意されている。テストを実装する ときには TestCase クラスのサブクラスでおこなう。 よって、Money クラス の実装をテストするために、 MoneyTest を TestCase のサブクラスとして定義することにしよう。Java ではクラスを パッケージに入れるので、 MoneyTest クラスをどのパッケージに置くか決め なくてはならない。ぼくたちの慣行では、 MoneyTest クラスをテスト対象と なっているクラス群と同じパッケージに入れることにしている。これなら、 テスト対象クラスのパッケージプライベートなメソッドに対するアクセスを テストケースから行うことができる。テスト用メソッドとして、 上記の簡易バージョン Money.add() を実行する testSimpleAdd というメソッドを追加 することにする。JUnit のテストメソッドは、引数無しの普通のメソッドである。
public class MoneyTest extends TestCase { //・ public void testSimpleAdd() { Money m12CHF= new Money(12, "CHF"); // (1) Money m14CHF= new Money(14, "CHF"); Money expected= new Money(26, "CHF"); Money result= m12CHF.add(m14CHF); // (2) assert(expected.equals(result)); // (3) } }testSimpleAdd() によるテストケース は、以下のものから構成される。
- テスト中に操作するオブジェクトを作成するコード。 これらテストのためのコンテクストを、ふつう「テストのフィクスチャ(fixture)」と呼んでいる。testSimpleAdd で必要になるものは、いくつかの Money オブジェクトだけだ。
- フィクスチャにあるオブジェクト群を実行するコード。
- 結果の検証を行うコード 。
public void testEquals() { Money m12CHF= new Money(12, "CHF"); Money m14CHF= new Money(14, "CHF"); assert(!m12CHF.equals(null)); assert(m12CHF, m12CHF); assertEquals(m12CHF, new Money(12, "CHF")); // (1) assert(!m12CHF.equals(m14CHF)); }Object の equals は、もし二つのオブジェクトが同一であれば true を返す。しかし、 Money は値を持つオブジェクト(value object) である。二つの Money は、 同じ通貨単位と金額を持っていれば等しいと見なされるのだ。この性質をテストするために、 テスト(1) を加えて、同一ではないが同じ金額を持つ二個の Money が、ちゃんと等しいと見な されるかどうか検証することにした。
次に Money クラスのほうに equals を加えることにしよう。
public boolean equals(Object anObject) { if (! anObject instanceof Money) return false; Money aMoney= (Money)anObject; return aMoney.currency().equals(currency()) && amount() == aMoney.amount(); }equals にはどんな種類のオブジェクトを引数として与えてもよいので、Money クラスにキャストする前に、まず型チェックを行う。別件だが、「equals メ ソッドをオーバーライドする時は常に hashCode メソッドも併せてオーバーラ イドせよ」ということが推奨されている。とはいえそれは省略して、ここでテストケースに戻ることにしよう。
equals メソッドが手に入ったので、testSimpleAdd の出力結果を検証する ことが可能になった。JUnit では検証を、TestCase クラスから継承された assert を呼び出すことで行う。引数が true でないとき、assert は「失敗(failure) 」を発生させ、これは JUnit によって記録される。
等値判定のためのアサート文はよくあるので、TestCase クラスは簡便のた めにassertEquals メソッドも用意している。これは、equals で等値の判定 を行うのみならず、等値でない場合に、二つのオブジェクトをプリントした 値をあわせてログに記録してくれる。これがあれば、テストが失敗したの はなぜか、JUnit のテスト結果レポートを見てすぐわかるというわけだ。値は、 toString という変換メソッドで生成された文字列表現として記録される。テストケースを二つ実装したところで、テストの準備を行うコードにダブ りがあることにぼくたちは気づいた。テスト準備のためのコードを再利用でき ればよいのに。別のことばで言えば、テストを行うための共用フィク スチャが欲しいということだ。JUnit でこれは、まずフィクスチャ内のオ ブジェクトを自分の TestCase クラスのインスタンス変数に保存することにし て、次に setUp メソッドをオーバーライドしてその中で初期化することで可 能である。 setUp 操作の反対となるのは tearDown で、これをオーバーライドすれば、 フィクスチャの後始末をテストの最後に行うことができる。
各テストはそれぞれ自分のフィクスチャを実行するので、 JUnit は setUp と tearDown を各テストごとに呼び出す。各テストランの間で 副作用が発生しないようにしている。public class MoneyTest extends TestCase { private Money f12CHF; private Money f14CHF; protected void setUp() { f12CHF= new Money(12, "CHF"); f14CHF= new Money(14, "CHF"); } }これで、二つのテストケースを書き直して、共通の準備コードを取り除くことができる:
public void testEquals() { assert(!f12CHF.equals(null)); assertEquals(f12CHF, f12CHF); assertEquals(m12CHF, new Money(12, "CHF")); assert(!f12CHF.equals(f14CHF)); } public void testSimpleAdd() { Money expected= new Money(26, "CHF"); Money result= f12CHF.add(f14CHF); assert(expected.equals(result)); }これらのテストケースを実行するには、さらに二つのステップが必要になる。
- 個別のテストケースを実行するやり方を決める。
- テストスィート(test suite)を実行するやり方を決める。
- 静的な(static)方法
- 動的な(dynamic)方法
TestCase test= new MoneyTest("simple add") { public void runTest() { testSimpleAdd(); } };スーパークラスにあるテンプレートメソッド [1]が、runTestを適切なタ イミングで実行することを保証している。
実行すべきテストケースを作成する動的な方法では、runTest の実装を リフレクションを用いて行う。前提として「テストの名前」は実行される「テストケースメソッドの名前」と同じになるものとする。この方法では、テストメソッ ドを動的に探し出し実行する。よって、testSimpleAdd を実行するために、以 下のような MoneyTest を作成することになる:
TestCase = new MoneyTest("testSimpleAdd");この動的な方法ならより簡潔に記述できるが、しかし静的な型に関しては、 より安全でなくなる。テストの名前を間違えてつけてしまったとしても、実 行して NoSuchMethodException が上がるまで気づかれないだろう。動的方法、 静的方法の両方にそれぞれ利点があるため、ぼくたちは「どちらを使うか」 をみなさんの選択に任せることにした。
二つのテストを一緒に実行するための最後のステップとして、テストスィー トを定義する必要がある。JUnit では、suiteと呼ばれるスタティックメソッ ドをこれののために書かなくてはならない。suiteメソッドは、テスト実行用 に作り込みを行ったmainメソッドのようなものだ。suite の内部では、 実行すべきテスト群を TestSuite オブジェクトに追加して、これを返す。 TestSuiteは「複数のテストの集まり」を実行することができる。 TestSuite も TestCase もどちらも、 Test インターフェースという、テストを実行するメソッドを定義したインターフェースを実装している。 だから、新たなテストスィートを、任意の TestCase や TestSuite を組み合 わせから作ることが可能になるのだ。短く言えば、TestSuite はComposite [1] に相当するのだ。以下 に示すコードは、テストを実行する動的な方法でテストケースを作成する方 法を示している。
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testMoneyEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); return suite; }これは、同等のコードを静的な方法で書いたものである。
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest( new MoneyTest("money equals") { protected void runTest() { testMoneyEquals(); } } ); suite.addTest( new MoneyTest("simple add") { protected void runTest() { testSimpleAdd(); } } ); return suite; }テストを実行する準備はこれで整った。JUnit には、テスト実行のためのグラ フィカルなインターフェースが付いている。テストクラスの名前を、ウィンド ウの上部にあるフィールドにタイプして、Run ボタンを押して欲しい。テスト 実行中にJUnit は、入力フィールドの下部にあるプログレス・バーによって進 展を表示してくれる。このバーは、最初は緑色だが、失敗に終わったテストが 出るとすぐ赤色に変わる。失敗したテストは下部にあるリスト上に表示される。 図1 は、 さっきの簡単なテストスィートを実行した後での TestRunner ウィンドウの様子だ。
図1: 成功したテスト実行
単一通貨の場合にうまくいったことを確認したあとは、複数通貨の場合に 移ろう。前にも述べたとおり、異なる通貨がからんだ算術の問題点は、固定 の交換比率というものがない、ということだった。この問題を避け、為替相 場による交換を先延ばしにするための MoneyBag というものをここで導入す ることにしよう。例えば、12 スイスフランを14 米ドルに加算した値は、12 CHF という Money と 14 USD という Money の入った袋(bag)として表現され る。さらに 10 スイスフランを足せば、袋の中身は 22 CHF と 14 USD にな る。後になってから MoneyBag を、さまざまな異なる為替レートで評価して やればよい。
MoneyBag は Money のリストとして表現され、さらにMoneyBag 作成用に、 いくつかのコンストラクタを提供している。ここで、コンストラクタがパッ ケージプライベートであることに注意されたい。というのも、MoneyBag は通 貨計算を行っている最中に、つまり「裏方」で作成されるものだからだ。
class MoneyBag { private Vector fMonies= new Vector(); MoneyBag(Money m1, Money m2) { appendMoney(m1); appendMoney(m2); } MoneyBag(Money bag[]) { for (int i= 0; i < bag.length; i++) appendMoney(bag[i]); } }appendMoney メソッドは内部ヘルパーメソッドである。これは Money のリス トに別の Money を追加すると共に、同じ通貨単位を持つ Money 同士の統合も 行う。MoneyBag には equals メソッドと、対応する equals のテストも必要 になる。ここでは equals メソッドの実装はとばして、testBagEquals メソッ ドのみを紹介することにしよう。まず最初のステップとして、フィクスチャに 二つの MoneyBag を追加することにする。
protected void setUp() { f12CHF= new Money(12, "CHF"); f14CHF= new Money(14, "CHF"); f7USD= new Money( 7, "USD"); f21USD= new Money(21, "USD"); fMB1= new MoneyBag(f12CHF, f7USD); fMB2= new MoneyBag(f14CHF, f21USD); }このフィクスチャをもとにして、testBagEquals テストは以下のように書かれ る:
public void testBagEquals() { assert(!fMB1.equals(null)); assertEquals(fMB1, fMB1); assert(!fMB1.equals(f12CHF)); assert(!f12CHF.equals(fMB1)); assert(!fMB1.equals(fMB2)); }"コードを少し、テストを少し"の原則に従って、この追加されたテストを JUnit で実行してみて、間違いを犯していないか検証することにしよう。 MoneyBag がすでにあるので、Money クラスの add メソッドを修正することが 可能になる。
public Money add(Money m) { if (m.currency().equals(currency()) ) return new Money(amount()+m.amount(), currency()); return new MoneyBag(this, m); }上記に示したように、このメソッドはコンパイルできない。というのも、メソッドの返り値として MoneyBag ではなく Money を返さなくてはいけないからだ。 MoneyBag を導 入したことによって、Money に対する二つの異なる表現が存在することになるが、 本来ならこれは、クライアントコードから隠蔽したいことである。そのために、 IMoney というインターフェースを導入し、二つの表現が両方ともこれを実装 するものとしよう。IMoney インターフェースは以下のようになる。
interface IMoney { public abstract IMoney add(IMoney aMoney); //・ }異なる表現の存在をクライアント側から完全に隠蔽しようとするなら、 Money と MoneyBag とのあらゆる組み合わせに関する算術をサポートしなくて はならない。だから、コードを書き進める前に、さらにいくつかのテストを定 義することにしよう。期待されるテスト結果を表す MoneyBag の作成には、前 に示した簡便用コンストラクタを使うことにする。これは、配列引数でもっ て MoneyBag オブジェクトの初期化を行う:
public void testMixedSimpleAdd() { // [12 CHF] + [7 USD] == {[12 CHF][7 USD]} Money bag[]= { f12CHF, f7USD }; MoneyBag expected= new MoneyBag(bag); assertEquals(expected, f12CHF.add(f7USD)); }ほかのテストも、同様のパターンをたどることになる: 次に、テストスィートのほうもさらに拡張する事にしよう:
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testMoneyEquals")); suite.addTest(new MoneyTest("testBagEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); suite.addTest(new MoneyTest("testMixedSimpleAdd")); suite.addTest(new MoneyTest("testBagSimpleAdd")); suite.addTest(new MoneyTest("testSimpleBagAdd")); suite.addTest(new MoneyTest("testBagBagAdd")); return suite; }テストケースを定義したので、その実装に取りかかることができる。 ここで実装を難しくしているのは、Money と MoneyBag との全ての異なる組み 合わせに対処しなくてはいけないということである。 二重ディスパッチ(Double dispatch) [2] という、この問題のエレガントな解法がある。二重ディスパッチの要点は、 いま扱っている引数の種類がなんであるか知るために、呼び出しをもう一回 行う、という点にある。最初の呼び出しの名前に、呼び出しを受けたオブジェ クトの名前をつなげて「メソッド名」を作成する。この名前で、引数オブジェ クトのメソッド呼び出しを行う。よって、add メソッドは以下のようになる:
class Money implements IMoney { public IMoney add(IMoney m) { return m.addMoney(this); } //… }
class MoneyBag implements IMoney { public IMoney MoneyBag.add(IMoney m) { return m.addMoneyBag(this); } //… }コンパイルできるようにするには、IMoneyインターフェースを拡張して、 二つのヘルパーメソッドを加える:
interface IMoney { //… IMoney addMoney(Money aMoney); IMoney addMoneyBag(MoneyBag aMoneyBag); }Money と MoneyBag とにこれらの実装部を書くことで、二重ディスパッチの実装が完了する。 これはMoney における実装だ。
public IMoney addMoney(Money m) { if (m.currency().equals(currency()) ) return new Money(amount()+m.amount(), currency()); return new MoneyBag(this, m); } public IMoney addMoneyBag(MoneyBag s) { return s.addMoney(this); }これは、MoneyBagでの実装だ。ただしMoney と MoneyBagとから一つの MoneyBag を、そして二つのMoneyBagから一つの MoneyBag を作成する 別のコンストラクタが用意されているものとする。
public IMoney addMoney(Money m) { return new MoneyBag(m, this); } public IMoney addMoneyBag(MoneyBag s) { return new MoneyBag(s, this); }テストを実行して、ちゃんと通った。しかし、実装したものをよく考えてみ ると、新たに面白いことに気づく。加算を行った結果、MoneyBag が Money を一つしか持たないバッグになってしまった場合は、どうなるのだろうか? 例えば、7 USD と 12 CHF とからなる Moneybag に、マイナス12 CHF を加算し た場合、7 USDだけからなるバッグになる。明らかに、このバッグは 7 USDを 持つ単一通貨のMoney と等しくならなくてはおかしい。この問題を検証する ために、テストケースを書いて走らせてみよう。
public void testSimplify() { // {[12 CHF][7 USD]} + [-12 CHF] == [7 USD] Money expected= new Money(7, "USD"); assertEquals(expected, fMS1.add(new Money(-12, "CHF"))); }このスタイルで開発をしていると、なんか思いついたら、直接コードをのぞく のではなくてすぐにテストを書いてみる、ということが多くなることに気づくだろう。
テストを実行したらプログレス・バーが失敗を表す赤色に変わってしまったが、これは予想の範囲内だ。MoneyBag のコードを直して、緑色に戻してやればよいのだ。
public IMoney addMoney(Money m) { return (new MoneyBag(m, this)).simplify(); } public IMoney addMoneyBag(MoneyBag s) { return (new MoneyBag(s, this)).simplify(); } private IMoney simplify() { if (fMonies.size() == 1) return (IMoney)fMonies.firstElement() return this; }またテストを実行して...... よっしゃ、緑に戻ったぞ。
上記のコードは、多国通貨の算術に関連する問題のほんの一部しか扱って いない。異なる交換レートを表現したり、異なるフォーマットで印字したり、 また加算以外の演算もサポートしたりして、さらにそこそこのスピードを維持 しなきゃいけなかったりするのだ。しかし、残りのオブジェクトを作成に関し ても、テストを一回ずつ増やしていく -- テストを少し、コードを少し、テス トを少し、コードを少し -- かたちでやる方法が、わかってもらえたと思う。
この開発例の中で、注意して思い出して欲しいことを並べてみよう:
- まず最初に testSimpleAdd というテストを、add() 関数を書き終えた直後に 作ってしまった。開発というものは、開発すると同時にテストも少しずつ書き加 えていったほうが、ずっとスムーズにいくものなのだ。 コードの動きについて考えをめぐらせている時というのは、まさにコーディング をしている最中にほかならない。だから自分の考えをテストにまとめるのは コーディング作業中が最適だ、ということになる。
- setUp という共通コードを書いてすぐ、いままであったテスト testSimpleAdd と testEqual とをリファクタしてしまった。テストコードといっても、 きちんとファクタリングして動かすのがベストだという点では、モデルのコードと 変わるところはない。同じテストコードが二カ所にち散らばっていることに気づいたら、 一カ所にまとめるようなリファクタリングが可能か、検討するべきだ。
- まず suite メソッドを書いて、二重ディスパッチ技法を応用した時に、それを拡張した。古いテストがきちんと通るようにすることは、新しいテストをきちんと 通るようにすることと同じくらい重要だ。テストの実行に時間がかかって、1時間に10回も 行うことすらできなくなるかもしれない。それでも、少なくとも1日に1回はかならず 全てのテストを実行するようにしよう。
- 要素が一つしかない MoneyBag は、その要素を返すようになっていなくてはおか しい、という要件に気づくやいなや、ぼくたちはすぐにテストを書いた。こういった「考え方のギアチェ ンジ」は、慣れるのが難しいかもしれない。だが、やる価値はあるのだ。システムは こう動くはずだ、という考えが頭に浮かんだとしても、実装について考えるのは後回 しにしよう。まずはテストを書くのだ。そしてテストを実行するのだ(もしかしたら、 一発ですぐ通ってしまうかもしれない)。その後に、実装のほうに目を向けるんだ。
テストする習慣(プラクティス)
マーチン・ファウラーが簡潔にまとめてくれている。 「print 文とか、デバッガ用 の式とか、そんなものを書きたいという衝動に駆られたら、かわりにテストを書こ う。」最初のうちは、新しいフィクスチャを毎回書かなくてはならないし、テスト 作業なんかしているとペースが遅れるように思うだろう。ところが、そのうちすぐに、 フィクスチャのライブラリを使い回すようになり、新たなテストの追加なんて すでにある TestCase のサブクラスにメソッドを一個くわえるだけの簡単なことに なってくるのだ。書こうと思えば何個テストを書いてもかまわない。でも、思い浮かんだテストの うち、実際に有用なのは一部だけだということにすぐ気づくだろう。欲しいのは、動 くはずと思っていたのに失敗するテストを書いたり、失敗するだろうと思っていた のに成功してしまうテストを書くことにあるのだから。これを、コスト・ベネフィッ トで考えてもよい。「情報」という見返りがあるテストを書きたいというわけだ。
テストという投資に対して、妥当な収益を見込めるような状況を、いくつかあげてみよう:
- 開発作業中 -- 新たな機能をシステムに追加する必要があるときには、まずテストを 書こう。このテストが通れば、作業も終わりということになる。
- デバッグ作業中 -- 誰かがシステムの欠陥を見つけたときには、まずは、 コードがきちんと動いていれば成功するようなテストを書こう。テストが成功するまで、 デバッグを続ければよい。
結語
この論文は、テスト作業のほんの一端を示したにすぎない。しかしこの論文は、非 常に少ない投資であなたを、より迅速で、より生産的で、より見通しが立てやすく、 よりストレスの少ない開発者に変えてしまう、そんなテスト作業のスタイルを選ん で取り上げたものである。ひとたびテスト熱中症(test infected)に感染したら、開発に臨むあなたの姿 勢が変わるに違いない。どんな違いがあらわれるかというと:
正しく通ったテストとそうでないテストは、天地ほども違ったものになる。テス ト熱中症の病状の一つは、テストが100%通るまではおうちに帰れない、という ものだ。とはいえ、一時間に十回も百回もテストをするようになった日には、もは や家の夕食に間にあわなくなるような大混乱をひきおこすことも無くなっているだ ろうが。
ときどき、とくに最初のうちは、テストを書きたくない気分になってしまうかも しれない。その場合はテストを書かない。でも、もしテストが無かったら、どれだ け大きなトラブルに自分を巻き込むことになるのか、どれだけの時間をデバッグに 費やすことになるのか、そしてどれほどのストレスを感じることになるのか、よく 考えたほうがいい。テストという支えがあれば、プログラミングがどれだけ楽しく なるか、ぼくたちがどれだけ積極果敢になれるか、そしてどれだけ、ストレスを感 じなくてすむようになるのか、びっくりするぐらいだ。気分がのらないって時も、 ぼくたちはテストを書く。この「違い」が、それだけ強烈だからだ。
テストがあると、より積極的なリファクタリングが可能にになる。しかし最初の うちは、どれくらい可能なのかは見えてこないだろう。であれば、自分がこんな風 にいう瞬間を見逃さないようにしよう:「ああ、わかった。本来はこう設計すべき だったんだ。でも今更変えられない。なにも壊したくないし。」もしこんな言葉が 出てきたら、現行コードのコピーを取った後に、コードのクリーンアップに二、三 時間かけてみよう(これは友達と協力して、自分の作業を横からのぞいてもらった ほうがうまくいく)。変更をくわえると同時に、テストも実行する。どこか壊しや しないかと常にビクビクしている状況でなければ、どれほどの範囲を二、三時間で カバーできるものか、自分で驚くはずだ。
例えば、ぼくたちは MoneyBag の実装を Vector を使ったものから HashTable を使う実装へと変更したことがある。この変更はすばやく自信を持って行うことが できた。なぜなら、それだけたくさんの頼れるテスト群があったからだ。もし全て のテストが通ったのなら、システムが吐き出す答えを変えるようなことをぼくたち は何もしなかった、ということが確認できるわけだ。
チームの同僚にもテストを書かせたい、という気になるかもしれない。経験から いって、テスト熱中症を広める最も良い方法は、接触感染だ。デバッグを手伝って くれ、という誰かのお願いが次に来たら、相手に自分がかかえている問題を、「フィ クスチャ」と「予想される結果」がわかるように説明してもらおう。そして「じゃ あ、説明してくれた問題を、利用可能なかたちに、書き出してみるよ」と言うんだ。 あなたが短いテストを書いているところを見てもらう。実行して、直して、別のテ ストも見たりする。すぐに相手も、自分で自分のテストを書くようになっているは ずだ。
というわけで -- ぜひ JUnit を試して欲しい。改良したところがあれば、みん なに配布できるよう、変更点をぼくたちに送って欲しい。次に書く論文では、JUnit のフレームワークそのものに「ダブルクリック」するつもりだ。フレームワークが どう構成されているか示し、またフレームワーク構築に関するぼくたちの哲学に ついても触れるつもりだ。
アナリストの中では最高のプログラマである、マーチン・ファウラー氏に感謝し たい。JUnit の初期版を使わされるというひどい目にあいながらも、彼は有益なコ メントをよこしてくれた。
レファレンス
- Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995
- Beck, K. Smalltalk Best Practice Patterns, Prentice Hall, 1996
翻訳: 小野 剛
ver 1.0 (ONO Sep/8/2000)