JavaEE7をはじめよう(17) - CDIのユニットテスト
CDIを使用したクラスのユニットテスト、すなわちJUnitなどのテストツールを使用した自動テストはどのように行うべきだろうか。 今回はユニットテストの方法について紹介する。
テスト対象のクラス
今回テストを行う対象は、前回の記事で紹介した、DAOを通じてデータベースを操作するSampleService
である。
以下に再掲する。
@Transactional @RequestScoped public class SampleService { // DAOをインジェクション @Inject private TeamDao dao; /** * 新しいチームを登録する * * 空文字の場合は実行時例外を、 * 引数が2文字以下の場合はチェック例外を発生させる。 * @param teamName チーム名 * @throws SampleException チーム名が2文字以下の場合 */ public void newTeam(String teamName) throws SampleException { Team t = new Team(); t.setName(teamName); dao.register(t); // 永続処理の実行後に、例外判定を行う。 if (teamName.equals("")) { throw new RuntimeException("uncheck exception throws."); } if (teamName.length() <= 2) { throw new SampleException("check exception throws."); } } public List<Team> allTeams() { return dao.findAll(); } }
CDIは単体テストを容易にする
このSampleService
はTeamDao
を利用しているため、TeamDao
に依存している。
また、TeamDao
もデータベースにアクセスするためにEntityManger
などに依存している。
ただし、依存しているとは言っても、SampleService
はTeamDao
のフィールドを宣言しているだけで、依存しているインスタンスを生成する処理は一切行っていない。
もし、依存しているインスタンスの生成をSampleService
内で行っていると、依存しているインスタンスの中身や初期化方法まで考慮する必要があるため、ユニットテストは非常に煩雑になる。
依存先ではデータベースにアクセスしているため、考慮すべきことはさらに増える。
SampleService
は様々なアノテーションが付いていることを除けば、フレームワーク特有のクラスなどの継承も行っておらず、依存するオブジェクトの生成・取得も行っていない、単なるPOJOである。
このため、アノテーションの問題をクリアできれば、単にSampleService
のコンストラクタを呼び出してインスタンスを生成しても問題は生じない。Servlet
のようなフレームワークなどから提供されるクラスを継承していると、そもそもインスタンス生成すら困難なケースがあるため、普通にインスタンスの生成ができることはユニットテストでは重要だ。
もちろんCDIによるインスタンス生成を無効にした場合、フィールドのTeamDao
はnull
のままなので、メソッドの実行はできなくなる。このため、POJOが依存しているフィールドにオブジェクトを設定する必要がある。
具体的な方法としては、依存先のオブジェクトにスタブやモックなどを設定するやり方がある。そうすることで、依存先の内容やデータベースなどの外部リソースに依存せず、テスト対象のロジックの検証に集中できるようになる。
このように、CDIによる疎結合は、クラスごとのユニットテストを容易にする。
モックによるユニットテスト
SampleService
が依存しているTeamDao
をモックに差し替えることによって、ユニットテストを行う例を紹介する。
ここではモックを作成する方法として、Mockitoを使用する。 Mockitoは、Java のモックテストツールとして広く普及している代表的なライブラリである。
Mockitoの詳しい使い方は割愛するが、次のような有用な機能がある。
- 継承・実装を行わずにモックを作成できる
- メソッドの振る舞いや検証を柔軟に設定できる
- インターフェースではなく既存のクラスからでもモックを作成できる
特に最後の機能は強力だ。
通常は、モックの差し替えを可能とするために、依存するオブジェクトをインターフェースと実装を分け、宣言にはインターフェースを指定することが多いだろう。
Mockitoを使えば、インターフェースと実装を分けなくてもモックを作ることができる。このため、テスト時にモックを使いたいというだけの理由であれば、全ての要素について、インターフェースと実装を分ける必要はなくなる。 DIを使用したことがある方であれば、際限なく増え続けるインターフェースとその実装クラス(xxxImpl) を見たことがあるのではないだろうか。
今回のサンプルでは、そのような理由でインターフェースと実装に分けていない。
テストクラスのサンプル
先ほどのSampleService
の allTeams
メソッドについてのテストクラスの例を以下に記載する。(newTeam
メソッドは後述する)。
public class SampleServiceTestByMock { @Mock // モックとして宣言し既存の振る舞いを変更する。 TeamDao dao; @InjectMocks // モックを注入するオブジェクトの宣言 SampleService target = new SampleService(); @Before public void setUp() { // @InjectMockが付いているオブジェクトのフィールドに // @Mockが付与されたモックを注入する。 MockitoAnnotations.initMocks(this); // モック daoのfindAllの振る舞いを変更。 when(dao.findAll()).thenReturn(Arrays.asList(TeamOf("A", 1), TeamOf("B", 2))); } /** * DAOをモック化してテスト実施。 */ @Test public void testAllTeams() { List<Team> result = target.allTeams(); assertThat(result.size(), is(2)); assertThat(result, is(Arrays.asList(TeamOf("A", 1), TeamOf("B", 2)))); } // 便利メソッド private static Team TeamOf(String name, long version) { Team t = new Team(); t.setName(name); t.setVersion(version); return t; } }
TeamDao
型の変数dao
には、@Mock
アノテーションを付与してある。このアノテーションは、TeamDao
のインスタンスをモックとして振る舞いを変更可能にすることを指す。
テスト対象のSampleService
のインスタンスは、普通にコンストラクタで生成している。
(この方法でユニットテストを行う場合、 CDI周りのアノテーションなどは、実行に一切関係ない。)
setUp
メソッドでは、MockitoAnnotations.initMocks
メソッドを実行している。このメソッドによって、@InjectMocks
アノテーションが付与されたSampleSevice
オブジェクト内のフィールドに対して、@Mock
が設定されたTeamDao
のモックオブジェクトを注入している。
(これは、Mockitoのアノテーションによるモック設定機能を使用した例で、アノテーション以外にメソッド呼び出しでモックを設定する方法もある。)
setUp
メソッドでは、変数dao
のfindAll
メソッドの戻り値を、2件のリストを返すように振る舞いを変更している。
これにより本来のメソッドは実行されなくなるため、TeamDao
の依存先のEntityManger
のことを考慮する必要はない。
testAllTeams
メソッドではallTeams
メソッドの戻り値を検証する。allTeams
メソッドは、TeamDao
のfindAll
メソッドの結果を返すのみである。
事前にモックによって、2件のリストを返すようになっているので、結果は2件のリストのはずだ。
本来allTeams
メソッドは、データベースから内容を取得する処理を行うが、モックによってデータベースを利用せず、固定の値を取れるようになっている。allTeams
メソッドに分岐などがあれば、それに応じてテストパターンを増やせばよい。
CDIも含めたユニットテスト
開発作業がある程度進むと、モックを使ったテストだけでは物足りなくなり、データベースとの連携を確認したい場合も出てくるだろう。 CDIのアノテーションの設定が正しく行われているかも確認したい場合があるかもしれない。
そのような場合、 CDIコンテナ(CDI 実行環境) を作成してJUnitを実行することが考えられる。
JUnitでCDIコンテナを動かすツールには、以下のようなものがある。
- Weld - Weldは、CDIの参照実装として、glassfishや WildFlyに使用されている。拡張として、 CDIを Java SEで使用可能な Weld-se があり、これであれば JUnitで実行可能だ。ただし、Java EE 特有の
@RequestScoped
などには対応していないため、自力で拡張を行う必要がある。 - CDI-Unit - 上記のWeld-se をラップし、前述の問題を解消したもの。
- Apache DeltaSpike - Apacheで開発されている CDI拡張ライブラリ群で、その中にユニットテスト用の機能がある。
- arquillian - アプリケーションサーバーを起動し、テスト機能を組み込むことで、CDIのテストやリクエスト単位のテストを行うツール。
今回は CDI-Unit を使ったの例を紹介する。理由はこのツールが、最も導入が簡単で機能が豊富だったためだ。
本来であれば、DeltaSpike のテスト機能を使用したかったのだが、設定が難しく、また Java EE7への対応が進んでいないためか動作するまでに至らなかった。
arquillian は実際にサーバーを稼動させるため、結合試験のようなより上位のテストを行うのに適している。このツールもいずれ解説したい。
CDI-Unitによるテストクラス
SampleService
のnewTeam
メソッドのテストを行うためのサンプルを掲載する。
このテストは、テスト用の組み込みデータベースを使用し、TeamDao
を通じてデータベースまでを連携させるテストを行う。
/** * CDIUnitによるDIを含めたテスト * * 宣言的トランザクションはサポートされないので、 * トランザクション制御は自前で行う。 */ @RunWith(CdiRunner.class) // CDIUnitの動作で必須。 public class SampleServiceTestByCDI { @Inject // テスト対象 宣言のみ行う。 SampleService target; // トランザクション制御で使用。targetの内部で使用するEntityMangerと共有。 @Inject EntityManager em; EntityManagerFactory emf; // 初期処理 @PostConstruct void init() { emf = Persistence.createEntityManagerFactory("ut"); } /** UT用のEntityManagerの生成 */ @Produces @ApplicationScoped EntityManager createUtEm() { System.out.println("called"); return emf.createEntityManager(); } /** トランザクションの開始 */ @Before // このアノテーションにより、各テストクラスのメソッドや、 // 対象オブジェクト内のインジェクション対象が共有される。 @InRequestScope public void setUp() { em.getTransaction().begin(); em.persist(TeamOf("A", 1)); em.flush(); } /** ロールバック */ @After @InRequestScope public void tearDown() { em.getTransaction().rollback(); } /** 1件登録を行うケース */ @Test @InRequestScope public void testNewTeamOnNormal() throws Exception { target.newTeam("B"); assertThat(em.find(Team.class, "B"), is(TeamOf("B", 1))); } /** チェック例外が起きるケース */ @Test(expected = SampleException.class) @InRequestScope public void testCheckException() throws Exception { try { target.newTeam("checkng"); } catch(SampleException e) { em.flush(); assertThat(em.find(Team.class, "checkng"), is(TeamOf("checkng", 1))); throw e; } } // 以下割愛
上のコードのポイントを簡単にまとめておく。
@RunWith(CdiRunner.class)
によりCDI-UnitがCDI実行環境を生成してテストを行う。- テストクラスの中で、
@Inject
によるインジェクションが行える(テストクラス自身もCDI Beanである)。 テスト対象クラスの他、EntityManger
のような他のCDIで管理されているオブジェクトも取得でき、プロパティの操作も行える。 - インジェクションしたオブジェクトの生存期間は、各メソッドで宣言する
@InRequestScope
のようなアノテーションで決まる。SampleService
はリクエストスコープのCDI Beanであるため、@InRequestScope
を宣言しておくことで、テストメソッドの単位で、CDI Beanのライフサイクルが設定される。 EntityManager
はUT用の設定を使用するため、テストクラス内にProducerメソッドを作成している。- トランザクション管理は自前で行う。
テスト用データベースの設定のために、別途設定ファイルを作成している。 これらの詳細は、以前の記事や githubのテストソースを参照いただきたい。
CDI-Unitの基本的な使い方を紹介した、その他の使用方法は公式サイトを参照いただきたい。
まとめ
今回紹介したのは、CDI Beanのテスト方法だが、JSFや JAX-RS, EJB なども、アノテーションを付与した単なるPOJOなので、今回と同じ方法でテストが可能だ。 Java EE はテストが難しいというのは、過去の話といってよいだろう。
[前多 賢太郎]