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

はじめに

  1. 自動テストとテストの分離
  2. モックオブジェクトを使ったテスト
  3. サンプル
  1. JMockitを用いたテストの実行
    1. Mavenからのテスト実行
    2. Gradleからのテスト実行

本ツールキットは, Maven Central リポジトリ上でアーティファクトのセットとして提供されています. テストの実行には,Java 7以降が必要です.テストにはJUnitまたはTestNGを使用しなければなりません. Javaプロジェクトへのライブラリの追加方法については, JMockitを用いたテストの実行を参照してください.

このチュートリアルでは,サンプルのテストコード(Java 8)を使用して,ライブラリで利用可能なAPIを紹介します. まず,中心となるAPI(単一アノテーション)では,テスト対象となるオブジェクトの自動インスタンス化・初期化をサポートします. 次に,モック API("Expectations" APIとも)では,依存関係をモック化してテストができます. 最後に,小さな フェイク API("Mockups" APIとも)では, 外部コンポーネントへの依存や影響を避けるためのフェイク実装を適用することができます.

チュートリアルはAPIすべてを詳細に網羅しているわけではありません.アノテーション,クラス,メソッドなどの完全で詳細な仕様は,APIドキュメントで提供しています."jmockit-1.x-sources.jar"ライブラリファイル(Maven Centralリポジトリからダウンロード可能)に,Javadocコメントを含むソースファイルが含まれており,Java IDEからソースコードやドキュメントに簡単にアクセスすることができます.

コードカバレッジ ツールについては別の章で説明します.

1 自動テストとテストの分離

テストコードは,開発者自身が自らのコードをテストするために記述するものです. ふつうJUnitTestNGといったテストフレームワークを使って記述されており,JMockitはいずれもサポートしています.

開発者による自動テストは,テスト対象の範囲により,大きく2つに分類することができます.

  1. 単体テスト(ユニットテスト).システムの他の部分から切り離し,クラスやコンポーネントをテストするためのもの.
  2. 結合テスト(インテグレーションテスト).ユニットと相互作用する他クラス/コンポーネントを含んだ動作をテストするためのもの. 結合テストは,一連のユニットをテストするものから,UIを操作して確認するようなシステムのテストまで広範囲に及びます. ここでは,UIを操作するようなものは考えません.

結合テストには,ユニット間の相互作用を含みますが,場合によっては関連するコンポーネントやサブシステムのことを考えたくないかもしれません. そのような場合,一部を「モック化(またはフェイク化)」し,テストに影響を及ぼさないようにする方法があります. こうした機能は,様々な種類のテストで役に立ちます. ただし,一般にテスト出来るだけ「現実的に」行うのが最善手です.ですから,モック化やフェイク化の使用は最小限に抑えておくのが理想です.

2 モックオブジェクトを使ったテスト

コードを単体で実行するための,一般的かつ強力な方法は「モック化」です. 伝統的なモックオブジェクトは,単体テストまたは関連するテストのために特別に用意されたクラスのインスタンスです. このインスタンスは,テスト対象のコードに渡すことで,依存をモックに置き換えます. 各モックオブジェクトは,テストコードやテスト対象のコードに合うよう動作するため,すべてのテストをパスすることができます. しかし,モックオブジェクトの役割はそれだけではありません. 各テストのアサーションを補完するために,モック自身が追加のアサーションを提供します.

JMockitは,従来のモックオブジェクトと異なり,メソッドコンストラクタのモックを「本物の」クラスに対して適用出来ます. テスト対象のコードにいちいちモックオブジェクトを渡す必要はありません. テスト対象のコードで作成された「本物の」クラスのコンストラクタやメソッドが呼び出されるたびに,テストコードで定義したモック動作が実行されます. クラスがモック化されると,そのテストを実行している間は,オリジナルの実装はモック実装に置き換えられます. モック化の手法は,publicなインスタンスメソッドだけでなく,finalstaticメソッド,コンストラクタに対しても適用可能です.

モックは分離された単体テストで最も威力を発揮しますが,結合テストでも利用可能です. 例えば,ある部分でメール送信を行うような操作をテストしたいとしましょう.メールが正しく送信されたことを確認するために,メール送信APIをモック化しつつ,それ以外は実際のコードを実行することができます(ダミーのメールサーバを用意してアサーションするという方法もあります).

3 サンプル

次のような業務を提供する業務サービスクラスを考えてみます.

  1. 操作に必要となるデータ(永続化されたエンティティ)を検索する
  2. 新しいエンティティの状態を保持する
  3. 関係者にメールを送信する

前半2つは,データベースへのアクセスが必要です.これは,アクセスを単純化するJPAのようなAPIを介して行われます. 最後の1つは,メール送信のためのサードパーティAPIを使って実現ができます(例: Apache Commons Email).

つまり,この業務サービスクラスは「DB」と「メール」の2つに依存します. この業務をテストするために,JMockitのJPAサポートを使用してDBに対応しつつ,メールAPIをモック化します. 問題に対応するソースコードの完全版は ここから利用可能 です.

package tutorial.domain;

import java.math.*;
import java.util.*;
import org.apache.commons.mail.*;
import static tutorial.persistence.Database.*;

public final class MyBusinessService
{
   private final EntityX data;

   public MyBusinessService(EntityX data) { this.data = data; }

   public void doBusinessOperationXyz() throws EmailException {
      List<EntityX> items =
(1)      find("select item from EntityX item where item.someProperty = ?1", data.getSomeProperty());

      // 新しいエンティティの値を算出または他サービスから取得
      BigDecimal total = ...
      data.setTotal(total);

(2)   persist(data);

      sendNotificationEmail(items);
   }

   private void sendNotificationEmail(List<EntityX> items) throws EmailException {
      Email email = new SimpleEmail();
      email.setSubject("XXX の処理に関するお知らせ");
(3)   email.addTo(data.getCustomerEmail());

      // メールサーバのホスト名などのパラメタは,
      // 外部でデフォルト値が定義されているものとします

      String message = buildNotificationMessage(items);
      email.setMsg(message);

(4)   email.send();
   }

   private String buildNotificationMessage(List<EntityX> items) { ... }
}

この Databaseクラスには,staticメソッドとprivateコンストラクタしかありません.findメソッドとpersistメソッドの処理は明らかなので省略します.

さて,既存のコードを変更することなく "doBusinessOperationXyz" メソッドをテストするにはどうすれば良いでしょうか? 下記のJUnitテストクラスでは,DBアクセスが正しく実行され,かつメールAPIが期待通り実行されたかをチェックしています. チェックしている箇所は,(1)から(4)の番号で示しています.

package tutorial.domain;

import org.apache.commons.mail.*;
import static tutorial.persistence.Database.*;

import org.junit.*;
import org.junit.rules.*;
import static org.junit.Assert.*;
import mockit.*;

public final class MyBusinessServiceTest
{
   @Rule public final ExpectedException thrown = ExpectedException.none();

   @Tested final EntityX data = new EntityX(1, "abc", "someone@somewhere.com");
   @Tested(fullyInitialized = true) MyBusinessService businessService;
   @Mocked SimpleEmail anyEmail;

   @Test
   public void doBusinessOperationXyz() throws Exception {
      EntityX existingItem = new EntityX(1, "AX5", "abc@xpta.net");
(1)   persist(existingItem);

      businessService.doBusinessOperationXyz();

(2)   assertNotEquals(0, data.getId()); // "data"がDBに保存されていることを意味する
(4)   new Verifications() {{ anyEmail.send(); times = 1; }};
   }

   @Test
   public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception {
      String email = "invalid address";
      data.setCustomerEmail(email);
(3)   new Expectations() {{ anyEmail.addTo(email); result = new EmailException(); }};
      thrown.expect(EmailException.class);

      businessService.doBusinessOperationXyz();
   }
}

このサンプルでは,JMockit APIのアノテーションをいくつか使用しています. @Tested はテスト用にオブジェクトが適切に初期化されるよう指定するもので, @Mocked は指定した型にモックを適用するものです.

このサンプルコードで示したように,new Expectations() {{ ... }}ブロックによる期待値の記録(指定)と,`new Verifications() {{ ... }}ブロックによる期待値の検証は,モック化された型やインスタンスに対してテストしたいメソッドを実行することによって機能します. メソッド等の返却値(または例外のスロー)は,期待値の記録(指定)を行う部分で "result" フィールドに指定します. 呼び出し回数の制限についても,期待値の記録または検証を行う部分で"times = 1"のようにフィールドに指定することができます.

4 JMockitを用いたテストの実行

JMockit APIを用いたテストは,Java IDEやMaven/Gradleビルドスクリプト等で行うのがふつうです. 原則として,Windows/Mac OS X/Linuxで,JDK 7以降を使用してください.また,JUnit(4または5)あるいはTestNGも必要となります. 具体的には,次のことが必要になります:

4.1 Mavenからのテスト実行

JMockitのアーティファクトはMaven Centralリポジトリにあります. テストスイートで使用するには,pom.xmlファイルに下記を追加してください:

<dependencies>
   <dependency>
      <groupId>org.jmockit</groupId>
      <artifactId>jmockit</artifactId>
      <version>${jmockit.version}</version>
      <scope>test</scope>
   </dependency>
</dependencies>

"jmockit.version"プロパティで指定したバージョンが,あなたが使いたいバージョンであることを確認してください. 最新バージョンは, 開発履歴ページで確認できます. JMockitは-javaagent JVM初期化パラメタの指定も必要です.Maven Surefireプラグインを使用してテストする場合,次のように指定します:

<plugins>
   <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.21.0</version> <!-- または別のバージョン -->
      <configuration>
         <argLine>
            -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
         </argLine>
         <!-- ... -->
      </configuration>
   <plugin>
   <!-- ... -->
<plugins>

MavenでのJMockit Coverageの使用の関連するセクションもご覧ください.

4.2 Gradleからのテスト実行

Gradleは,mavenCentral()リポジトリから必要なアーティファクトをダウンロードできます. gradle.buildファイルにjmockit dependencyとテストの設定を追加します.バージョン指定は 必要に応じて適切なものを指定してください.

repositories {
    mavenCentral()
}

def jmockitVersion = '1.45'

dependencies {
   ... "compile" dependencies ...
   testCompile "org.jmockit:jmockit:$jmockitVersion"
}

test {
    jvmArgs "-javaagent:${classpath.find { it.name.contains("jmockit") }.absolutePath}"
}

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