CDIを使ったプログラムの単体テスト(1)

これから何回かに分けて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はそのような場合に活用できると思います。

DeltaSpikeモジュールの概要

久しぶりのブログへの投稿になります。当ブログではJava EE仕様のバックボーン的な重要な仕様であるContexts and Depenency Injection (CDI)とその拡張モジュールについて書いています。特に、CDI拡張モジュールについてはDeltaSpikeの活動について注目しています。しばらく更新が滞っていましたが、WildFly 10などJava EE 7ベースのアプリサーバーが普及するとアプリケーション開発の現場でCDIを使う機会が益々増えると思いますので、またブログを再開したいと思います。今ままで同様、不定期の更新になるかとは思いますが、今後ともよろしくお願いします。

DeltaSpikeはポータブルなCDI拡張モジュール

Apache DeltaSpikeはオープンソースで開発されているCDIの拡張モジュール集です。DeltaSpikeは、CDIコンテナの代表的な実装であるWeldやOpenWebBeansの両方で動作し、以下のアプリサーバー上でテストが行われています(Jenkins上でのビルドはこちら)。

  • JBoss AS7
  • JBoss WildFly 8
  • Oracle GlassFish 3
  • Oracle GlassFish 3.1
  • Oracle GlassFish 4
  • Oracle WebLogic 12c

このようにDeltaSpikeの大きな魅力の一つは特定のCDIコンテナやアプリサーバーに依存しないポータルブルなモジュールを開発しているということです。DeltaSpikeはオープンソースですので、ユーザからのフィードバックを得て機能を改善していきます。これらの中で有用なものは将来のCDI仕様やJava EE仕様そのものに影響を与えていくと思います。実際、CDI 2.0 (JSR-365)の活動の中でもDeltaSpikeからのフィードバックが期待されています。その意味で、DeltaSpikeはすぐに使える便利ライブラリを提供するプロジェクトというだけでなく、CDIの将来の動向を知る上でも重要なプロジェクトだと言えるでしょう。

DeltaSpikeの提供モジュール

1年前に投稿したブログの記事ではDeltaSpikeのバージョン1.0について書きましたが、現在ではバージョン1.5まで開発が進んでいます。ここで、今ままでの振り返りを兼ねて、現在のDeltaSpikeの提供モジュールについてご紹介します。以下の表はDeltaSpike 1.5が提供するモジュールの概要になります。

表1:DeltaSpike 1.5のモジュール
モジュール 説明
Core DeltaSpike APIとユーティリティクラスを定義する基盤となるもの
Bean Validation EJBやManagedBeanのようなビジネスオブジェクでバリデーションをするときに使えるCDIを使ったBeanバリデーション
Container Control CDIコンテナの起動/シャットダウンと関連するコンテキストのライフサイクル管理をするためのもの
Data ボイラープレートなコードを減らすために宣言的クエリを使えるようにJPAを改善するためのもの
JPA トランザクション・コンテキストとスコープのためのもの
JSF タイプセーフなビューの構成、マルチウィンドウ処理、新しいスコープ(WindowScoped, ViewScope, ViewAccessScoped, GroupedConversationScoped)などCDIを使ったJSF向けの統合
Partial-Bean インタフェースや抽象クラスを手で実装する代わりにジェネリックハンドラーを実装するためのもの
Scheduler Quartz v2(デフォルト)やジョブクラスのためのcron式をサポートする他のスケジューラとのシンプルな統合のためのもの
Security メソッド呼び出し時に割り込んでセキュリティをチェックするためのもの
Servlet Servletオブジェクトでインジェクションを可能にしServletイベントをCDIイベントとして伝播させるようにするServlet APIとの統合のためのもの
Test-Control CDIベースのテストを簡単に書けるようにするためのもの

DeltaSpikeモジュールの使い方

DeltaSpikeの機能は独立した複数のモジュールとして提供されています。各モジュールはMavenのライブラリとして提供されていて、開発者はモジュールを選択してpom.xmlの依存ライブラリとして設定します。つまり、アプリケーション開発者はそのプロジェクトが必要とするモジュールだけを選択して取り込むことが可能です。

Java EEアプリケーションであればJSFやJPAのモジュールを含むDeltaSpikeのフル機能を使うことができます。しかし、JSFやJPAを使わないJava EEアプリケーションであってもDeltaSpikeを十分活用することができます。GUIはJSFではなくてJavaScriptライブラリを使うという場合は、JSF以外のモジュールについては利用可能です。Java SEアプリケーション開発の場合は、CDIコンポーネント開発を楽にするユーティリティや単体テストのためにCoreモジュールを使うことができるでしょう。

当ブログでは、今後、DeltaSpikeの代表的なモジュールについて紹介していこうと考えています。CDIを使って開発をするときの単体テストの方法は切実な問題だと思います。さらに、テスト時に、本番環境とは異なるテスト専用のBeanに差し替える方法も重要です。次回はこれらのテーマをカバーするCoreモジュールのProjectStageとTest-Controlモジュールについて紹介する予定です。