JMockit Java言語のための自動テストツールキット

業務アプリケーションのテスト

  1. サンプル
    1. Java EEの利用
    2. テストの土台(共通クラス)
    3. Spring Frameworkの利用
  1. インタフェースの解決
    1. テスト対象のオブジェクトを明示する
    2. インタフェースを解決するメソッドを用意する
  2. 手法のトレードオフ

業務 アプリケーション は一般に,特定の業種をターゲットにし,GUIを複数ユーザが同時利用し,数多くのエンティティ(テーブル)を持つデータベースを持ち,さらには組織内外の他のアプリケーションと連携することも珍しくありません. Javaでは,Java EEやSpring Frameworkといったものがよく使われます.

この章では,コンテナ外の結合テストによる,業務アプリケーションのテスト手法を説明します. いわゆる ビジネスシナリオ,あるいはユースケースや "利用シナリオと呼ばれるものの1ステップをテストするためのものです. 典型的な階層化アーキテクチャでは,最上位のレイヤ(ふつうアプリケーション層)のコンポーネントから下位のレイヤのpublicメソッドを呼び出します.

1 サンプル

デモンストレーションのために,Java EEで作ったSpring Pet Clinicというサンプルアプリケーションを使います. 完全なコードはプロジェクトのリポジトリにあります. アプリケーションは4つのレイヤで構成されています.UI(プレゼンテーション)レイヤ,アプリケーションレイヤ,ドメインレイヤ,インフラレイヤの4つです.

このアプリケーションのドメインモデルには,6つのドメインエンティティがあります: Vet(獣医師),Specialty(診療科),PetPetTypeOwner(飼い主),Visit(訪問). (※用語や考え方は Domain Driven Designを参照) エンティティのほかに,アプリケーションのドメインモデル(及びレイヤ)にはドメインサービスクラスもあります. このシンプルなドメインでは,1つのエンティティタイプに1つのクラスだけを定義します(VetMaintenancePetMaintenance等).

DDD(ドメイン駆動設計)では,エンティティは「リポジトリ」コンポーネントによって永続化ストレージに追加・変更・削除されます. JPAのような洗練されたORM APIを使えば,リポジトリコンポーネントは1つで済みます.ドメインやアプリケーション固有ではないインフラレイヤの Database クラスです. アプリケーションはリレーショナル・データベースを使用します.サンプルアプリケーションではインメモリ型のHSQLDBデータベースを使用しており,アプリケーション内にデータベースを内包しています.

アプリケーションレイヤにはアプリケーションサービスクラスを含みます.これは,UIからのユーザの入力を解釈して下位レイヤを呼び出したり,出力結果をUIに表示したりします. このレイヤはデータベースのトランザクション境界(単位)となります.

1.1 Java EEの利用

Java EE 7では,ドメインの@Entity型にJPA,ドメインサービスにEJB(ステートレスなSessionBean)またはシンプルな@Transactionalクラス,アプリケーションサービスにはJSF @ViewScoped Beanを使用します. サンプルにはUIレイヤのコードは含まれていません(結合テストの対象ではないため). (※Java EEでは,UIレイヤは".xhtml"形式のJSF faceletsで構成されます.)

最初の結合テストでは,獣医師(Vet)画面上で,すべての獣医師とその診療科を表示することを考えてみましょう.

public final class VetScreenTest
{
   @TestUtil VetData vetData;
   @SUT VetScreen vetScreen;

   @Test
   public void findVets() {
      // 入力としてデータベースに獣医師と診療科のデータを登録する
      Vet vet2 = vetData.create("Helen Leary", "radiology");
      Vet vet0 = vetData.create("James Carter");
      Vet vet1 = vetData.create("Linda Douglas", "surgery", "dentistry");
      List<Vet> vetsInOrderOfLastName = asList(vet0, vet1, vet2);

      // テスト対象のコードを実行する (VetScreen, VetMaintenance, Vet, Specialty).
      vetScreen.showVetList();
      List<Vet> vets = vetScreen.getVets();

      // 出力結果が期待値通りか検証する
      vets.retainAll(vetsInOrderOfLastName);
      assertEquals(vetsInOrderOfLastName, vets); // リストの内容と順序をチェック

      Vet vetWithSpecialties = vets.get(1); // "vet1"は...
      assertEquals(2, vetWithSpecialties.getNrOfSpecialties()); // ...2つの診療科を担当

      vetData.refresh(vetWithSpecialties); // DBに存在する獣医師のデータを再取得
      List<Specialty> specialtiesInOrderOfName = vetWithSpecialties.getSpecialties();
      assertEquals("dentistry", specialtiesInOrderOfName.get(0).getName()); // 診療科をチェック...
      assertEquals("surgery", specialtiesInOrderOfName.get(1).getName()); // ...正しい順序であること
   }
}

最初に注意すべき点は,このテストはアプリケーションの最上位層として実行されており,Javaで書かれている―つまり,アプリケーションはJVM上で動作しているということです. ですから,HTTPリクエスト・レスポンスや,URLがアプリケーションにどうマッピングされているか,といった点は無視できます.そうした細かい点は,UIの実装に使われる技術(JSF, JSP, Struts, GWT, Spring MVC等)によって異なってくるため,結合テストでは考えないようにします.

次に注意しなければならないのは,テストコードは非常にクリーンで,テスト対象が明確になっていることです. テストはアプリケーションや業務ドメインについてはきちんと記述している一方で,低レベルなデプロイメントやデータベース設定やトランザクションといった点は一切記述していません.

最後に注意すべき点は,JMockit APIをテストクラス内で全く意識していないことです. 使ったのは@TestUtil@SUTのアノテーションのみです. これらはユーザ定義のアノテーションで,チーム内で任意の名前にできます.サンプルコートでは,次のように定義しました.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Tested(availableDuringSetup = true, fullyInitialized = true)
public @interface TestUtil {} // テストユーティリティ

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Tested(fullyInitialized = true)
public @interface SUT {} // テスト対象のシステム

これにより,JMockitの @Tested アノテーションは メタアノテーションとして使われています. "fullyInitialized = true" と記述すると,Dependency Injection(依存性注入)によって対象クラスの依存関係が(コンストラクタとそこに含まれるフィールドのインジェクションにより)自動解決されます. "availableDuringSetup = true" は,テスト対象のオブジェクトを @Before(Junit) や @BeforeMethod(TestNG) で指定したセットアップメソッドを実行するより前に 生成します.指定しない場合,デフォルトではセットアップメソッドの後,各テストメソッドを実行する前にオブジェクトを生成します. サンプルのテストクラスでは,これらの効果は特に活用しておらず,単にフィールドの意味や目的を明文化しただけになっています.

テストクラスから分かるように,VetData はテストに必要なデータを作成したり,データベースから最新のエンティティをロードする refresh(an entity) メソッドがあったりします. その名前が示すように,テストスイートはエンティティタイプごとにこうしたクラスを持ち,永続化インスタンスが必要なテストクラスで使用されます. 詳細は次のセクションで説明します.

1.2 テストの土台(共通クラス)

VetDataのようなユーティリティクラスは次のようになっています:

public final class VetData extends TestDatabase
{
   public Vet create(String fullName, String... specialtyNames) {
      String[] names = fullName.split(" ");

      Vet vet = new Vet();
      vet.setFirstName(names[0]);
      vet.setLastName(names[names.length - 1]);

      for (String specialtyName : specialtyNames) {
         Specialty specialty = new Specialty();
         specialty.setName(specialtyName);

         vet.getSpecialties().add(specialty);
      }

      db.save(vet);
      return vet;
   }

   // 必要に応じて異なるデータを取る "create" メソッド を記述する
}

こうしたクラスは簡単に記述できます.既存のエンティティクラスを使い,親クラス TestDatabase のフィールド "db" のメソッドを使うだけです. これは,永続化にJPA,そして結合テストにJMockitを使っている限り,他の業務アプリケーションでも再利用可能な共通クラスと言えます.

public class TestDatabase
{
   @PersistenceContext private EntityManager em;
   @Inject protected Database db;

   @PostConstruct
   private void beginTransactionIfNotYet() {
      EntityTransaction transaction = em.getTransaction();

      if (!transaction.isActive()) {
         transaction.begin();
      }
   }

   @PreDestroy
   private void endTransactionWithRollbackIfStillActive() {
      EntityTransaction transaction = em.getTransaction();

      if (transaction.isActive()) {
         transaction.rollback();
      }
   }

   // 他のユーティリティメソッド(refresh, findOne, assertCreated等)
}

本番環境でも利用できる Database ユーティリティクラスは,JPAのEntityManagerクラスをより使いやすくしたものです. 利用は任意です. テストでは,"db"フィールドの代わりに"em"フィールドを直接使うこともできます. EntityManager emフィールドは,テストのランタイムクラスパス(Maven互換のプロジェクト階層であれば"src/test",ちなみに本番環境向けには"src/main")にあるMETA-INF/persistence.xmlファイルに従ってインスタンスは自動的に生成・注入(inject)されます. EntityManagerのデフォルトインスタンスは1つだけ生成され,テストまたは本番環境の @PersistenceContextフィールドを持つクラス(Databaseとか)に注入されます. 複数のデータベースが必要な場合,異なるEntityManagerを持つように,アノテーションの"name"属性を設定し,それに対応するエントリをpersistence.xmlファイルに記述します.

このクラスのもう1つの重要な役割は,テストごとにトランザクションを分離するとともに,テスト開始前にトランザクションを開始し,テスト終了後に(テストが成功しようが失敗しようが)ロールバックして終了するよう保証することです. これは,JMockitが @PostConstructメソッドと@PreDestroyメソッド(javax.annocation 標準APIのもの.Spring Frameworkでもサポート)を適切なタイミングで実行することで機能しています. テストデータのオブジェクトは,@Tested(availableDuringSetup = true)フィールドのテストクラスに設定され,セットアップメソッドやテストメソッドの前に「構築」され,テスト終了時に「破棄」されます.

1.3 Spring Frameworkの利用

@Autowired@Value といったSpring固有のアノテーションも, @Tested オブジェクトでサポートされています. ただし,Springベースのアプリケーションでは BeanFactory 実装クラスのインスタンスで BeanFactory#getBean(...) メソッド を直接呼び出すこともできます.

Beanファクトリインスタンスの取得方法にかかわらず, @Tested オブジェクトと @Injectable オブジェクトはBeanファクトリインスタンスからBeanとして利用できます. mockit.integration.springframework.FakeBeanFactory フェイククラスを適用することで,JUnitを使って下記のようにします

public final class ExampleSpringIntegrationTest
{
   @BeforeClass
   public static void applySpringIntegration() {
      new FakeBeanFactory();
   }

   @Tested DependencyImpl dependency;
   @Tested(fullyInitialized = true) ExampleService exampleService;

   @Test
   public void exerciseApplicationCodeWhichLooksUpBeansThroughABeanFactory() {
      // テスト対象のコード:
      BeanFactory beanFactory = new DefaultListableBeanFactory();
      ExampleService service = (ExampleService) beanFactory.getBean("exampleService");
      Dependency dep = service.getDependency();
      ...

      assertSame(exampleService, service);
      assertSame(dependency, dep);
   }
}

Beanファクトリのフェイクを適用すると,テストクラスのフィールドにあるテスト対象のオブジェクトは,Bean名とフィールド名が一致していれば, 任意のSpring BeanファクトリインスタンスのgetBean(String)を実行して自動的に返却します.

さらに,テストクラスから@Testedオブジェクトとして指定することで,mockit.integration.springframework.TestWebApplicationContextクラスは,org.springframework.web.context.ConfigurableWebApplicationContext実装クラスとして利用することができます.

2 インタフェースの解決

一部のアプリケーションコードでは,アプリケーション固有の実装クラスの多くにインタフェースを使用します. これらのインタフェースは,注入された依存関係を受け取るフィールドやパラメタで使用されるものです. そのため,インタフェースベースの依存関係を持つ@Testedオブジェクトをインスタンス化するときは, インタフェースを実装するクラスをJMockitに教える必要があります.方法は2つあります.

2.1 テスト対象のオブジェクトを明示する

2.2 インタフェースを解決するメソッドを用意する

3 手法のトレードオフ

このテスト手法では,業務アプリケーションのJavaコードすべてを網羅する結合テストを行うことをゴールにしています. TomcatやGlassfish, JBoss WildflyといったJavaアプリケーションサーバで実行することによる固有の問題を避けるため,テストコード・本番コードが同じJVMインスタンスで実行されるコンテナ外テストとしています.

テストはアプリケーションの最上位コンポーネントのAPIに対して記述されています. そのため,Java以外で記述されたり,まちまちなテンプレート言語で記述されるようなUIのコードはテストされていません. JavaScriptを含むようなWebアプリケーションのUIコンポーネントをテストするには,WebDriverやHtmlUnit APIを使い,HTTPリクエストとレスポンスをベースにした機能のテストを記述する必要があります. こうしたテストはコンテナ内の手法で,アプリケーションサーバの起動方法やデプロイ方法,テストの独立性を維持する方法,あるいはデータベースのトランザクションがコミットされてしまうなど,様々な問題や難しさをはらんでいます.

対照的に,コンテナ外の結合テストは,ちょうど良い粒度で,テスト終了後に常にロールバックされるトランザクション構成されています. この手法は,作成が容易で,かつ実行が早く(特に起動コストが小さい),堅牢なテストを可能にします. さらに,単一のJVMインスタンスで実行されるため,コードカバレッジツールやデバッガの利用が容易です. 欠点としては,UIテンプレートコードやクライアントサイドで動作するJavaScriptコードなどがテストで網羅できないことが挙げられます.


翻訳: たいぷらいたー(にゃみかん),ライセンス表示: LICENSE.txt