JUnit用アサーション・エクステンション

alphaWorksのパッケージを使うと、複合アサーションを持つユニット・テストが容易に

Comments

人気のJUnit自動ユニットテスト・フレームワークを使うと、「そのコードは目的とする要求に合致している」というアサーションを行うことによって、ソフトウェアのコード・ユニットをテストできるようになります。ところがこうしたアサーションは、「2つの変数に対して等しいとアサートする」とか「参照変数はヌルではないとアサートする」などの基本的な操作に限定されています。基本的なJUnitアサーションは便利ですが、実世界のソフトウェアのユニットテスト・シナリオで必要となるような、多くの複雑なアサーション機能を表すものではありません。

JUnitフレームワーク用のエクステンション・パッケージであるJUnitX(Assertion Extensions for JUnit)は、alphaWorks(参考文献)からダウンロードすることができます。JUnitXは、多くの一般的な複合アサーションで要求される実装を提供しています。ですからアサーション用に複雑なJUnitテスト・ケースを書く代わりに、JUnitXメソッドを呼び、(なんら追加設定無しに)同じコンテキストからアサーションを行うことができるのです。JUnitXはまた、独自のJUnitセルフテスト・スイートを含めることによって、ドキュメント通りに機能することをアサートします。これによって、JUnitXがJUnitXドキュメンテーションに従ってアサートしていることが改めて確認できます。つまりユニット・テストがフェールした場合には、JUnitXテスト実装が誤ったフェールをアサートしているのではなく、ソフトウェアのコード・ユニットがフェールしたのだと分かるのです。

JUnitXが有用となる典型的なシナリオとして、java.lang.Objectクラスのequals(Object)メソッドとhashCode()メソッドが開始するコントラクト(contracts)があるでしょう。皆さんが開発するクラスの中で、こうしたコントラクトを守るように要求される場合がよくあります。コントラクトを守ったアサーションをJUnitを使って行おうとすると、複雑なユニットテスト・ケースを開発する必要が出てきますが、これはエラーにつながりがちです。対照的にJUnitXを使ってアサーションを行うと、クラスのインスタンスを返すファクトリー実装を作り、ユニットテスト・ケースからJUnitXのassertEqualsContractメソッドとassertHashCodeContractメソッドを呼ぶだけ、というように簡単になります。

JUnitXの初歩

JUnitXを効果的に使うために学ぶべきことは、殆どありません。JUnit自動ユニットテスト・フレームワークを直接使う方法を知っている人であれば、JUnitXエクステンション・パッケージを使うのは簡単なはずです。最初のステップは下記の通りです。

  1. まず、JUnitテスト・ケースを実行するための環境を設定します。これには、「Automating the build and test process」(参考文献)に詳述されている手順に従った方が良いかも知れません。
  2. JUnitXパッケージ(参考文献)をダウンロードし、JUnitXアーカイブを自分の好きなディレクトリーで解凍します。
  3. lib/junitx.jarファイルが、JUnitテストを行うクラス・ローダーで使えるようにします。

こうすると、junitx.framework.Assertクラスに対してメソッド・コールをし、通常のJUnitテスト環境でjunit.framework.Assertクラスを使うのと似た方法で機能をアサートすることができます。JUnitXのオンラインAPIドキュメンテーション(参考文献)には、junitx.framework.Assertクラスで使用できるメソッド・コールの詳細が説明されています。

ユース・ケース・シナリオ

皆さんが、ある人を表現するクラスを実装するように要求されたと思ってください。Personクラスには、その人の肩書き(title)、名前(first name)と名字(surname)、という3つの属性が必要です。肩書き属性として考えられる値は、MR、MS、MRSのいずれかなので、Titleクラスをタイプセーフ列挙(Typesafe Enumeration)設計パターンを使って実装することにします。図1のUML図は、こうした要求事項の現状を示しています。

図1. 要求のUML図
図1. 要求のUML図

リスト1は、こうした要求に対するソース・コードを示しています。

リスト1. 必要なクラスに対するソース・コード
public class Person {
  private Title title;
  private String firstName;
  private String surName;
}

public class Title {
  public static final Title MR = new Title();
  public static final Title MS = new Title();
  public static final Title MRS = new Title();
  // private constructor to prevent outside instantiation
  private Title() {
  }
}

ここでソース・コードは、堅牢かつ完全に機能するようになっている必要があります。そのためには、下記に示すような典型的な要求事項を含めて、より具体的な要求事項を満足しなければなりません。

  • Personクラスは、コレクション・タイプ(Collection types)で効果的に使えるように、コントラクトに従ってequals(Object)メソッドとhashCode()メソッドをオーバーライドする。
  • Personクラスはjava.io.Serializableインターフェースを実装し、なんらエラーを生じることなく、直列化と非直列化を行う。
  • Personクラスは、サブクラス化できるように、finalとしては宣言しない。
  • Titleクラスは何のコンストラクターもエクスポーズしないため、finalと宣言される(つまりTitleクラスはサブクラス化できないことを意味します)。final修飾子がソース・コードや生成されたAPIドキュメンテーションの中で見えるように、この設計判断を理解していることを文書化しておくことのは良い習慣と言うことができます。
  • Titleクラスはjava.io.Serializableインターフェースを実装し、なんらエラーを生じることなく直列化と非直列化を行い、また(タイプセーフ列挙設計パターンの要求に従って)同じインスタンスへと直列化する。
  • Titleクラスは、クラス外からのインストールを防ぐために、privateデフォルト(つまり属性のない)コンストラクターを持つ。

こうした要求は全て、JUnitテスト環境でJUnitXを使うことによってアサートすることができます。リスト2のソース・コードは、「JUnitXの機能を使って全要求事項に合致できている」とアサートするJUnitテスト・ケースの一式です。

リスト2. JUnitXアサーションを使うJUnitテスト・ケース
import junit.framework.TestCase;
import junitx.framework.ObjectFactory;
import junitx.framework.Assert;
import java.io.Serializable;
import java.lang.reflect.Constructor;
public class TestRequirements extends TestCase {
  public void testPersonEqualsAndHashCodeContract() {
    // Different surnames should be unequal.
    ObjectFactory factory = new ObjectFactory() {
      public Object createInstanceX() {
        return new Person(Title.MR, "Bob", "Brown");
      }
      public Object createInstanceY() {
        return new Person(Title.MR, "Bob", "Smith");
      }
    }
    // Make sure the object factory meets its contract for testing.
    // This contract is specified in the API documentation.
    Assert.assertObjectFactoryContract(factory);
    // Assert equals(Object) contract.
    Assert.assertEqualsContract(factory);
    // Assert hashCode() contract.
    Assert.assertHashCodeContract(factory);
  }
  public void testPersonSerialization() {
    // Assert that the Person class directly implements Serializable.
    Assert.assertDirectInterfaceOf(Person.class, Serializable.class);
    // Assert that the Person instance can be serialized and deserialized without errors.
    Assert.assertSerializes(new Person(Title.MR, "Joe", "Blog"));
  }
  public void testPersonNotFinal() {
    // Assert that the Person class is not declared final.
    Assert.assertNotFinal(Person.class);
  }
  public void testTitleFinal() {
    // Assert that the Title class is declared final.
    Assert.assertFinal(Title.class);
  }
  public void testTitleSerialization() {
    // Assert that the Title class directly implements Serializable.
    Assert.assertDirectInterfaceOf(Person.class, Serializable.class);
    // Assert that the Title instances can be serialized and deserialized without errors.
    Assert.assertSerializes(Title.MR);
    Assert.assertSerializes(Title.MS);
    Assert.assertSerializes(Title.MRS);
    // Assert that serialization results in the same instance.
    Assert.assertSerializesSame(Title.MR);
    Assert.assertSerializesSame(Title.MS);
    Assert.assertSerializesSame(Title.MRS);
  }
  public void testTitleConstructor() {
    // Assert that the Title class has a default constructor.
    Assert.assertClassHasConstructor(Title.class, null);
    try {
      // Get the default constructor.
      Constructor con = Title.class.getDeclaredConstructor(null);
      // Assert that the default constructor is declared private.
      Assert.assertPrivate(con);
    }
    catch(NoSuchMethodException nsme) {
      // Should never get here, even when test fails.
      throw new IllegalStateException();
    }
  }
}

リスト1のPersonクラスとTitleクラスは、全ての追加要求事項は満足しないため、リスト2のテスト・ケースはパスしません。今度は、ユニット・テスト・ケースがパスする(要求事項合致を示します)ように、新しい要求事項に合致するクラスを開発します。リスト3は、規定された要求事項に合致する実装の例を示しています。

リスト3. 追加の要求事項に合致するように改訂されたクラス
import java.io.Serializable;
public class Person implements Serializable {
  private Title title;
  private String firstName;
  private String surname;
  public Person(Title title, String firstName, String surname) {
    this.title = title;
    this.firstName = firstName;
    this.surname = surname;
  }
  public boolean equals(Object o) {
    // Performance optimization only.
    if(this == o) {
      return true;
    }
    if(o == null) {
      return false;
    }
    if(!(o instanceof Person)) {
      return false;
    }
    Person p = (Person)o;
    return title == p.title & firstName.equals(p.firstName) & surname.equals(p.surname);
  }
  public int hashCode() {
    final int oddPrime = 461;
    int result = 73;
    result = result * oddPrime + title.hashCode();
    result = result * oddPrime + firstName.hashCode();
    result = result * oddPrime + surname.hashCode();
    return result;
  }
}

import java.io.Serializable;
public final class Title implements Serializable {
  public static final Title MR = new Title();
  public static final Title MS = new Title();
  public static final Title MRS = new Title();
  private static int nextIndex = 0;
  private final int index = nextIndex++;
  private static final Title[] VALUES = new Title[]{MR, MS, MRS};
  private Title() {
  }
  // Ensure that the same instance is returned when deserialized.
  Object readResolve() {
    return VALUES[index];
  }
}

リスト3のクラスでJUnitテスト・ケースを実行すると、テストをパスします。これによって、与えられた要求事項にコードが合致していると結論することができます。

まとめ

JUnitXを使わずに、コードに対するユース・ケース・シナリオでアサートを行おうとすると、(JUnitXを使うよりも)ずっと大量の作業が必要となります。テスト・ケースで失敗した場合には、不正なソフトウェア・コード・ユニットではなく、不正なテスト・ケースが構成された事を意味し、診断のために、さらに作業が必要となります。一方JUnitXテスト・ケースがフェールし、ソフトウェア・コード・ユニット中の欠陥がすぐに明確に分からない場合には、一緒に含まれているセルフテスト・スイートのソース・コードを読んで、テスト・ケースをパスするコード・ユニットはどう見えるのかを調べればよいのです。

より多くの機能を要求されるのに合わせて、JUnitXパッケージは今後も進化を続けます。


ダウンロード可能なリソース


関連トピック

  • alphaWorksからAssertion Extensions for JUnitをダウンロードしてください。
  • オープン・ソースのJUnit自動ユニット・テスト・フレームワークの全てについて学んでください。
  • Automating the build and test process」(developerWorks, 2001年)では、AntとJUnitを使って、ビルドとテストのプロセスを自動化、カスタム化する方法を示しています。
  • DbUnitとAnthillによるテスト環境の制御」(developerWorks, 2004年)は、各テストの前にデータベース状態を設定することによって、エンド・ツー・エンドでテスト環境を制御するためにJUnitとDbUnitを組み合わせる方法を解説しています。
  • Use Jython to build JUnit test suites」(developerWorks, 2004年)は、静的に定義されたJUnit TestSuiteクラスに見せかけるためにPythonを使う方法を解説しています。
  • AntとJUnitを用いた漸進的開発」(developerWorks, 2000年)を読むと、ユニット・テストを使って漸進的にコードを改善できるようになります。
  • developerWorksのJava technologyゾーンには、他にもJava技術に関する資料が豊富に用意されています。
  • Developer BookstoreにはJava関連の書籍を始め、広範な話題を網羅した技術書が豊富に取り揃えられていますので、是非ご利用ください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=224143
ArticleTitle=JUnit用アサーション・エクステンション
publish-date=03082005