CDIを使ったプログラムの単体テスト(1)
2015/10/31 コメントを残す
これから何回かに分けてCDIベースのテストを支援するDeltaSpikeのモジュールを紹介します。今回はDeltaSpike Test-Controlモジュールです。
CDIベースの単体テストの問題
CDIを使ったインジェクション、プロデューサ、インタセプタ、イベントなどを使ったクラスをどうやってテストしたらよいのでしょう。setterインジェクションを使うなど、無理をすればある程度はコンテナ無しでもテストができるかもしれませんが、できればCDIの普通の使い方をしたままでテストをしたいものです。そうなると、JUnitとCDIコンテナを連携させる仕組みが必要になります。
テスト対象がEJBやコンテナ管理のJPAを使っている場合など、Java EEコンテナに依存する場合には、Arquillianを使うのが定番ですが、Arquillianはテストをアプリサーバー上で実行するためにアプリサーバーの起動・停止が必要となり、どうしてもテストの実行時間が長くなる傾向があります。
ここで紹介するDeltaSpike Test-Controlモジュールは、CdiTestRunnerというJUnitのTestRunnerとして提供されるモジュールで、JUnitと組み合わせて使うことで、Java SE上でCDIのテストができます。CdiTestRunnerは、ArquillianのようにJava EEの機能を使ったテストはできませんが、Java SE上でのテストですのでArquillianに比べて短時間にテストを実行することができるというメリットがあります。
CdiTestRunnerの使い方
使い方は簡単です。まず、Mavenでプロジェクトを作って、以下のような依存ライブラリを設定したpom.xmlファイルを設定します。このpom.xmlではCDIコンテナとしてWeldを使うことを想定していますが、他のコンテナを使うときにはマニュアルを参照してください。また、バージョンについては以下のサンプルでは現時点での最新バージョンを指定しましたが、お使いのプロジェクトに合わせて変更してください。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.tanoseam.example</groupId> <artifactId>tanoseam-example</artifactId> <version>1.0-SNAPSHOT</version> <properties> <junit.version>4.12</junit.version> <weld.version>2.3.1.Final</weld.version> <deltaspike.version>1.5.1</deltaspike.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-test-control-module-api</artifactId> <version>${deltaspike.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-test-control-module-impl</artifactId> <version>${deltaspike.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.deltaspike.cdictrl</groupId> <artifactId>deltaspike-cdictrl-weld</artifactId> <version>${deltaspike.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se-core</artifactId> <version>${weld.version}</version> <scope>test</scope> </dependency> </dependencies> </project>
次に、テストクラスでは@RunWithにCidTestRunnerを指定します。以下はTest-Controlの使用例を示すために作ったインジェクションを使った簡単なサンプルです。
package org.tanoseam.examples; import org.junit.Test; import org.junit.runner.RunWith; import javax.inject.Inject; import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner; @RunWith(CdiTestRunner.class) public class CdiUnitTest { @Inject private MyBean myBean; @Test public void testLog() { myBean.log("hello"); } }
CdiUnitRunnerはRequestScope/SessionScope/ApplicationScopeをサポートしています。このテストがインジェクトしているMyBeanはSessionScopeのBeanです。このMBeanは、さらにApplicationScopeのLoggingBeanというBeanをインジェクトしています。
package org.tanoseam.examples; import java.io.Serializable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.enterprise.context.SessionScoped; import javax.inject.Inject; @SessionScoped public class MyBean implements Serializable { @Inject LoggingBean loggingBean; public MyBean() {} @PostConstruct public void initialize() { log("MyBean::initialize"); } @PreDestroy public void destroy() { log("MyBean::destroy"); } void log(String message) { loggingBean.log(message); } }
LogginBeanの定義は次のようになります。
package org.tanoseam.examples; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class LoggingBean { public LoggingBean() {} @PostConstruct public void initialize() { log("LoggingBean::initialize"); } @PreDestroy public void destroy() { log("LoggingBean::destroy"); } void log(String message) { System.out.println("CDI: " + message); } }
最後に忘れてはいけないのがbeans.xmlです。以下のファイルをMETA-INF/beans.xmlに配置してください。ここではbean-discovery-modeとして”annotated”を指定しています。このモードのときはCDIコンテナはスコープアノテーションがついてるクラスしかスキャン対象にしませんので注意してください。
※ beans.xmlにversion=”1.1″を付け忘れていたので追記しました(11/02)
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" version="1.1" bean-discovery-mode="annotated"> </beans>
このテストを実行するとコンソールに以下のように表示されます。実際にはWeldの起動や停止の部分のログが大量に出力されますが、ここではテストプログラムの出力前後のみを掲載します。
10 31, 2015 4:25:19 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted 情報: [run] org.tanoseam.examples.CdiUnitTest#testLog CDI: LoggingBean::initialize CDI: MyBean::initialize CDI: hello 10 31, 2015 4:25:19 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished 情報: [finished] org.tanoseam.examples.CdiUnitTest#testLog CDI: MyBean::destroy CDI: LoggingBean::destroy 10 31, 2015 4:25:19 午後 org.jboss.weld.environment.se.WeldContainer shutdown
CdiTestRunnerでは、RequestScopeやSessionScopeのBeanはテストごとに作成されます。上のテストでは、MyBeanはSessionScopedなのでテストごとに生成されますが、LoggingBeanはApplicationScopeなので、一回作成されると複数のテスト実行中に同じものが使いまわされます。
ここでスコープの違いによるBeanのライフサイクルの違いを見るためにテストプログラムの一部を以下のように変更します。ご覧の通り、単に同じテストを2回実行しているだけです。
@Test public void testLog() { myBean.log("myBean=" + myBean); //myBean.log("hello"); } @Test public void testLog2() { myBean.log("myBean=" + myBean); }
実行後のコンソールログを見ると、LoggingBean::initializeとLoggingBean::destroyはそれぞれテストの先頭と最後にしか呼び出されていないのに対し、MyBean::initializeとMyBean::destroyはテストメソッドの呼び出しごとに呼び出されていることが確認できます。
10 31, 2015 4:36:23 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted 情報: [run] org.tanoseam.examples.CdiUnitTest#testLog CDI: LoggingBean::initialize CDI: MyBean::initialize CDI: myBean=org.tanoseam.examples.MyBean@67c33749 10 31, 2015 4:36:23 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished 情報: [finished] org.tanoseam.examples.CdiUnitTest#testLog CDI: MyBean::destroy 10 31, 2015 4:36:23 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted 情報: [run] org.tanoseam.examples.CdiUnitTest#testLog2 CDI: MyBean::initialize CDI: myBean=org.tanoseam.examples.MyBean@fa49800 10 31, 2015 4:36:23 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished 情報: [finished] org.tanoseam.examples.CdiUnitTest#testLog2 CDI: MyBean::destroy CDI: LoggingBean::destroy 10 31, 2015 4:36:23 午後 org.jboss.weld.environment.se.WeldContainer shutdown
CDIのDI(Dependency Injection)は、もともとBean間を疎結合連携をさせるための仕組みです。アプリケーション全体としてはJava EE上でないと動作できない場合であっても、それを構成する個々のBeanについてはJava SE上で単体テストが十分可能な場合もあるでしょう。今回紹介したCdiTestRunnerはそのような場合に活用できると思います。