JUnit の新しいインスタンス

2004年8月24日

私はよく、JUnit テストフレームワークにおける設計上の選択肢の1つ、つまり、テストメソッドの実行ごとに新しいオブジェクトを作成するという決定について質問を受けます。簡単な bliki エントリに値するほどです。(しかし、JUnit について書くことは、他の形式のテストが重要でないと思っているわけではないことを指摘せざるを得ません。多くの有用なテスト活動があり、JUnitとその仲間はその多くに役立ちますが、すべてに対する解決策ではありません。テストに関するブログをもっと見たい場合は、Brett PettichordBrian MarickJames Bach のブログをチェックすることをお勧めします。また、xUnit テストについて私が書いているからといって、リファクタリング、ユースケース、またはフロス(歯磨き)の重要性が低いという意味ではないと仮定しないでください。)

次の簡単な Java テストクラスについて考えてみましょう。

import junit.framework.*;
import java.util.*;

public class Tester extends TestCase {
  public Tester(String name) {super(name);}
  private List list = new ArrayList();
  public void testFirst() {
    list.add("one");
    assertEquals(1, list.size());
  }
  public void testSecond() {
    assertEquals(0, list.size());
  }
}

一部の人々はこれに気づいていないかもしれませんが、両方のテストはパスし、どちらの順序で実行してもパスします。これは、JUnit がこのテストを実行するために 2つ の Tester のインスタンスを作成し、各 testXXX メソッドに1つずつ作成するためです。したがって、リストフィールドは、テストメソッドの実行ごとに新たに初期化されます。さて、このことを JUnit の バグ だと思う人もいますが、そうではありません。これは意識的な設計上の決定です。(この種の詳しいことについては、ケントの新しい本をチェックしてください。)

JUnit の基本的な設計は、ケント・ベックが Smalltalk で構築したテストフレームワークに由来します。(実際、それをフレームワークと呼ぶのは少し誤解を招きます。ケントはそれをフレームワークとして出荷したことはありません。彼は、1〜2時間しかかからないので、自分で構築することを好みました。そうすれば、何か違うことがしたいときに変更することを恐れないからです。)JUnit の重要な原則の1つは、分離です。つまり、あるテストが他のテストを失敗させるようなことをしてはいけないということです。

分離にはいくつかの利点があります。

  • テストの任意の組み合わせを、同じ結果で任意の順序で実行できます。
  • あるテストが失敗した理由を調べていて、原因が別のテストの記述方法にあるという状況に陥ることはありません。
  • あるテストが失敗しても、他のテストを失敗させるような残骸を残すことを心配する必要はありません。これにより、本当のバグを隠してしまうカスケードエラーを防ぐことができます。

さて、JUnit は分離をサポートする他のメカニズムを提供します。特に、各テストメソッドの開始時と終了時に実行される setUp および tearDown メソッドです。このことを私の簡単な例で使用するには、次のようにします。

  public void setUp() {
    list = new ArrayList();
  }
	

setUp が必要な再初期化を行うことができるため、ほとんどの場合、tearDown を使用する必要はありません。

すべての状態をローカル変数に入れて、フィールドをまったく使用しないことで、テストメソッドを分離できます。ただし、これは、すべてのテストで setUp コードを複製することを意味します。そして、私が重複をどれほど軽蔑しているかを知っているでしょう。

JUnit のアプローチの批判者は、setUp および tearDown があるため、毎回新しいオブジェクトを作成する必要はないと主張しています。これらのメソッドですべてのフィールドを再初期化するようにすればよいだけです。JUnit のアプローチの支持者は、これは真実かもしれませんが、多くの人々がフィールドで初期化を行っており、このより高いレベルの分離を提供する方が良いと主張しています。結局のところ、フレームワーク設計の重要な部分は、正しいこと(分離)を簡単に行えるようにし、問題を引き起こすことを難しく(不可能ではない)することです。結局のところ、それを行うコストは何でしょうか?

JUnit のアプローチのコストに関する主な議論は、作成される追加のオブジェクト、つまり JUnit テストケースと、設定およびフィールドイニシャライザで作成される他のすべてのオブジェクトに基づいています。ほとんどの場合、この議論は無意味だと思います。多くのオブジェクトを作成することについての多くの恐れがありますが、ほとんどの場合、それは正当化されません。オブジェクトの割り当てとコレクションがどのように機能するかについての時代遅れのメンタルモデルに基づいています。確かに、オブジェクトの作成が問題になる可能性のある環境はあり、初期の頃の Java はその1つでした。しかし、最近の Java はほとんどオーバーヘッドなしでオブジェクトを作成できます。それはもはや問題ではありません。(Smalltalk ではより長く問題ありませんでした。そのため、ケントとエリックはそれを気にしませんでした。)したがって、ほとんどの場合、オブジェクトの作成について心配しないでください。

とは言え、ほとんどの場合、必ずしもそうではありません。頻繁に作成したくないオブジェクトの良い例の1つは、データベース接続です。これは共有するのが理にかなっていますが、1つのテストケースクラス内のすべてのテストメソッドで共有するだけでは十分ではありません。それよりもはるかに多くのテストケースで共有する必要があります。これを行うための安価で汚い方法は、static 変数を使用することです。一般的に、静的変数を避けるのが賢明ですが、テストの実行のコンテキストでは問題ないことがよくあります。ただし、私はそれらを避けることを好みます。JUnit は実際には、テストフィクスチャオブジェクトを共有するための非常に柔軟なメカニズムを提供しています。それは、TestSetup デコレータです。これにより、任意のテストスイートの共通状態を設定でき、単一のテストケースクラス内のメソッド間で共有するよりもはるかに柔軟にテストグループ間で状態を共有できます。

おそらく、TestSetup の最大の問題点は、それに関する情報を見つけるのが非常に難しいことです。ドキュメントに「ヒョウに注意」と書かれているだろうと予想したほどです。そして、ヒョウは確かにいます。TestSetup を使用すると、分離が損なわれ、壊れた分離は発見しにくいバグにつながる傾向があります。本当に、本当に必要な場合を除いて、使用しないでください。(ただし、使用する場合は、このフォーラムスレッドが、J.B.レインズバーガーの新しい本と同様に、使用に関するヒントを提供します。)

(これらすべてにより、なぜ各テストメソッドが独自のクラスにないのか疑問に思うかもしれません。実際、JUnit の初期の形式では、フィクスチャを使用してテストケースをサブクラス化する内部クラスを使用して、それを行っていました。これはより明白な設計でしたが、テストを書くのが難しくなりました。そこで、より不明確な プラグ可能なセレクターパターンを使用することにしました。)

JUnit のアプローチに対する2番目の異議は、それが直感的ではないということです。つまり、それがこれを実現するために使用するメカニズムは理解するのが難しいということです。私はこれに共感します。プラグ可能なセレクターパターンはあまり知られておらず、見慣れないパターンを使用する設計スタイルは、しばしば不快です。全体として、分離とテストの書きやすさが、難解な実装よりも重要であると考えているため、JUnit のアプローチが好きです。

しかし、良い人々は私に同意しません。セドリック・ブーストのTestNG はこれを行いません。おそらく驚くべきことですが、人気のある NUnit 実装もこれを行いません(ただし、ジムは現在、その決定を後悔しています)。次の NUnit テストは失敗を引き起こします。

  [TestFixture]
  public class ServerTester
  {
    private IList list = new ArrayList();
    [Test]
    public void first() {
      list.Add(1);
      Assert.AreEqual(1, list.Count);
    }
    [Test]
    public void second() {
      Assert.AreEqual(0, list.Count);
    }
  }

このスタイルで動作するフレームワークを使用している場合は、セットアップメソッドですべてのインスタンス変数を初期化することを強くお勧めします。そうすることで、テストを分離し、デバッグによって引き起こされる脱毛を回避できます。

私はテストケースインスタンスを再利用することに賛成しませんが、そうする決定を下した人が1桁の IQ を持っていたり、複雑な財政的殺害を計画していたり、下半身で奇妙な行動を始めたりしているとは思っていません。彼らは設計上のトレードオフを異なるように呼びました。そして、ソフトウェア設計の流動的な性質について、私たちが敬意を持って意見を異にできるとき、人生はより良いものになると思います。