単体テストのためのCDIコンテキスト制御

これまでCDIを使った単体テストについて何回か紹介しましたが、そこでは主にインジェクションの対象を切り替える方法について書いてきました。しかし、CDI(Contexts and Dependency Injection)のインジェクションは、単なるインジェクションではなく、インジェクトされるBeanはコンテキストが管理しているのが特徴です。CDIでは、コンテキストの寿命が尽きれば、それにバインドしたBeanも解放されます。そこで、テストケースとしてコンテキストの開始・停止をすることでBeanのライフサイクルを制御をしたいことがあると思います。今回は、DeltaSpikeのCDITestRunnerを使ったときのコンテキストの制御について紹介します。

ContextControl

CDITestRunnerを使ったテストコードでは、ContextControlを使ってApplication/Session/Requestのスコープを制御することができます。

@Inject
private ContextControl contextControl;

ContextControlには以下の機能があります。

  • startContexts()    すべてのコンテキストの開始
  • stopContexts()    すべてのコンテキストの停止
  • startContext(Class<? extends Annotation> scopeClass)  指定されたスコープのコンテキストの開始
  • stopContext(Class<? extends Annotation> scopeClass)  指定されたスコープのコンテキストの停止

ContextControlのサンプルコード

次のサンプルコードは、ApplicationScopeのLoggerとSessionScopeのShoppingCartを題材としてContextControlによるコンテキスト制御をした例になります。

@RunWith(CdiTestRunner.class)
public class ContainerControlTest {
	
	@Inject 
	private Logger logger; // ApplicationScoped

	@Inject
	private ShoppingCart shoppingCart; // SessionScoped

	@Inject
	private ContextControl contextControl;
	
	@Test
	public void testScope() {	
		shoppingCart.put(new Product("001", 1));
		shoppingCart.put(new Product("002", 3));
		shoppingCart.put(new Product("003", 7));
		logger.info("ShoppingCart=>" + shoppingCart);
		contextControl.stopContexts();
		contextControl.startContexts();
		logger.info("ShoppingCart=>" + shoppingCart);
		assertTrue(shoppingCart.isEmpty());
	}
}

CdiTestRunnerでは、テストメソッドtestScope()が呼び出される前に、すべてのコンテキストは自動的に開始されます。この例では、 すべてのコンテキストを明示的に停止するためにcontextControl.stopContexts()を呼び出します。これにより、 ApplicationScopeのLoggerとSessionScopeのShoppingCartはコンテキストから解放されます。次に、続いて contextControl.startContexts()を呼び出すことで、すべてのコンテキストが開始されます。この後ShoppingCart を参照すると、新しいコンテキストにバインドするために新規に生成されますので、その結果として、カートに登録したプロダクトは空に初期化されます。

以下はテスト実行時に出力されたコンソールログの抜粋です。Loggerは@PostConstructでLogger::initialize、@PreDestroyのメソッドでLogger::destroyをコンソールに出力しています。ShoppingCartについても同様にコンソール出力します。コンソール出力を見ると、contextControl.stopContexts()によってShoppingCartとLoggerの両方がdestroyされていることが確認できます。

11 24, 2015 11:35:28 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.ContainerControlTest#testScope
Logger::initialize
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[Product [productId=001, quantity=1], Product [productId=002, quantity=3], Product [productId=003, quantity=7]]]
ShoppingCart::destroy
Logger::destroy
Logger::initialize
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[]]
11 24, 2015 11:35:28 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.ContainerControlTest#testScope
ShoppingCart::destroy
Logger::destroy

上のサンプルコードでは、ApplicationScopeのLoggerも解放してしまっていますが、本来であれば、実際の動きに近づけるためにApplicationScopeのBeanについてはメソッドの先頭で生成し、メソッド終了時に解放したいところです。次の例は、RequestScopeとSessionScopeのコンテキストを個別に制御することで、ApplicationScopeを停止しないようにしています。

	@Test
	public void testScope() {	
		shoppingCart.put(new Product("001", 1));
		shoppingCart.put(new Product("002", 3));
		shoppingCart.put(new Product("003", 7));
		logger.info("ShoppingCart=>" + shoppingCart);
		contextControl.stopContext(RequestScoped.class);
		contextControl.stopContext(SessionScoped.class);
		contextControl.startContext(SessionScoped.class);
		contextControl.startContext(RequestScoped.class);
		logger.info("ShoppingCart=>" + shoppingCart);
		assertTrue(shoppingCart.isEmpty());
	}

以下がこのテストのコンソール出力です。ApplicationScopeのLoggerについて、Logger:initializeが先頭で、Logger.destroyが最後に出力されています。

11 24, 2015 11:40:26 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.ContainerControlTest#testScope
Logger::initialize
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[Product [productId=001, quantity=1], Product [productId=002, quantity=3], Product [productId=003, quantity=7]]]
ShoppingCart::destroy
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[]]
11 24, 2015 11:40:26 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.ContainerControlTest#testScope
ShoppingCart::destroy
Logger::destroy
広告

CDIによるプロパティのインジェクション

アプリケーションの設定情報をソースコード中にハードコードするのではなく、プロパティファイルに記述しておいて後から変更できるようにしたい場合があります。このプロパティの値をCDIらしくインジェクションで取得する方法としてProducerを使う方法があります。こうやって手作りするのは良いのですが、複数の変数の型に対応させたり、拡張可能にしたりと機能を盛り込んでいくと手間がかかります。今回はこのような機能を提供するDeltaSpike Configurationについて紹介します。

DeltaSpike Configurationとは

DeltaSpike Configurationはプロパティから構成情報を取得するための機能でCoreモジュールに含まれます。アプリケーションからこれを使うのは簡単です。変数のインジェクション時に@ConigurePropertyというアノテーションを指定すると、nameで指定された名前に応じたプロパティの値を変数にインジェクションしてくれます。

@Inject
@ConfigProperty(name = "serverAddress")
String serverAddress;

デフォルト値を設定したい場合はdefaultValueも一緒に指定します。

@Inject
@ConfigProperty(name = "serverAddress" , defaultValue = "localhost")
String serverAddress;

プロパティファイルのデフォルトはMETA-INF/apache-deltaspike.propertiesです。この例では、このファイルには以下のような該当するプロパティを書いておきます。

serverAddress=192.168.0.1

実は、DeltaSpike Configurationでは、apache-deltaspike.properties以外の場所でもプロパティを設定することが可能です。プロパティを設定をする方法として以下が提供されています。複数同時に設定されている場合はordinalが大きい方が優先されます。

表1:DeltaSpike Configurationのプロパティ設定箇所
プロパティのソース ordinal 備考
システムプロパティ 400
環境変数 300
JNDI 200 “java:comp/env/deltaspike/”
プロパティファイル 100 “META-INF/apache-deltaspike.properties”

@ConfigPropertyでサポートされる型

@ConfigPropertyでサポートされる変数の型は以下になります。

  • String
  • Integer
  • Long
  • Float
  • Double
  • Boolean

カスタムの型に対応させたい場合はConfigResolver.Converterインタフェースを実装します。実装例はドキュメントを参照してください。

@ConfigPropertyの例

プロパティを取得する具体例を見てみましょう。次のような文字列と数値のプロパティをあらかじめMETA-INF/apache-deltaspike.propertiesに設定しておきます。

username=tanoseam
pollingInterval=120

以下のテストコードを実行してみます。2番目、3番目の@ConfigPropertyの型はStringではなくIntegerです。このように文字列以外の値についてはプロパティ値をターゲットの変数の型に変換してからインジェクションしてくれるので便利です。

@RunWith(CdiTestRunner.class)
@TestControl(projectStage = ProjectStage.UnitTest.class)
public class ConfigurationTest {

	@Inject
    @ConfigProperty(name = "username", defaultValue = "tanoseam")
    String username;

    @Inject
    @ConfigProperty(name = "pollingInterval", defaultValue = "60")
    Integer pollingInterval;
    
    @Inject
    @ConfigProperty(name = "nullValue")
    Integer nullValue;

	@Test
	public void testConfig() {
		assertEquals("tanoseam", username);
		assertEquals(120, pollingInterval.intValue());
		assertNull(nullValue);
    }
}

このテストは問題なく成功するはずです。

プロパティの優先順位を確認するため、テスト起動時にシステムプロパティとして別の値を設定してみましょう。

mvn test -Dusername=neverbird

こうすると、今度はテストが失敗します。システムプロパティの方がプロパティファイルよりも優先されてインジェクトされているのがわかります。

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.475 sec <<< FAILURE!
testConfig(org.tanoseam.examples.ConfigurationTest)  Time elapsed: 0.007 sec  <<< FAILURE!
org.junit.ComparisonFailure: expected:<[tanoseam]> but was:<[neverbird]>
	at org.junit.Assert.assertEquals(Assert.java:115)
	at org.junit.Assert.assertEquals(Assert.java:144)
	at org.tanoseam.examples.ConfigurationTest.testConfig(ConfigurationTest.java:33)

 

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

CDIを使った単体テストについての3回目はDeltaSpike Test-ControlモジュールのMockフレームワークの使い方について紹介します。前回のブログでは@ExcludeとProjectStageを組み合わせることで開発ステージに合わせてインジェクトするBeanを切り替える方法について説明しましたが、今回紹介する方法ではもっとダイレクトにMockを使ってインジェクトの対象を切り替えます。

Mockフレームワークとは

使い方を説明するために、まずインジェクションを実行する舞台となるBeanを定義します。サンプルコードとしてMyLogicというBeanを作ります。このBeanはget()メソッドの引数の10倍の数を返すもので、内部でRepositoryのsave()メソッドを呼び出します。

package org.tanoseam.examples;

import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import java.io.Serializable;

@SessionScoped
public class MyLogic implements Serializable {
	
	public MyLogic() {}
	
	@Inject
	Repository repository;
	
	public int get(int value) {
		int result = value * 10;
		repository.save("something");
		return result;
	}
}

Repositoryは次のようなシンプルなインタフェースです。

package org.tanoseam.examples;

public interface Repository {
	public void save(Object obj);
}

そして、これを実装するRdbRepositoryというBeanを定義します。メソッドが呼び出されたことだけが確認できれば良いのでメソッドの内容はプリント文だけにしておきます。

package org.tanoseam.examples;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class RdbRepository implements Repository {
	public void save(Object dummy) {
		System.out.println("RdbRepository is called");
	}
}

このようなケースにおいてMyLogicからRdbRepositoryをインジェクトするのではなく、 テスト時にはその代わりにMockのオブジェクトをインジェクトするようにします。

DeltaSpikeのCdiTestRunnerでMockを扱うにはDynamicMockMangerを使います。これには手でMockオブジェクトを定義する方法とMockフレームワークを使う方法があります。

手でMockオブジェクトを定義する方法

次のサンプルコードは、手でMockオブジェクトを定義する方法です。DynamicMockMangerのaddMock()メソッドを使って、既存のクラスであるRdbRepositoryの無名サブクラスを定義します。これで、RdbRepositoryのインスタンスの代わりに、この無名クラスのインスタンスがインジェクトされるようになります。

@RunWith(CdiTestRunner.class)
public class DynamicMockManagerTest {
	
	@Inject
	MyLogic mylogic;
	
	@Inject
    DynamicMockManager mockManager;

	@Test
	public void testMock() {
        mockManager.addMock(new RdbRepository() {
        	@Override
        	public void save(Object obj) {
                System.out.println("Mock of RdbRepository is called");
            }
        });
		assertEquals(100, mylogic.get(10));
    }
}

Mockの使用を有効にする

DeltaSpikeではこのMockの機能はDELTASPIKE-872のためにデフォルトでは有効になっていません。使えるようにするためには以下の2つのシステムプロパティを設定することで有効になります。

  • deltaspike.testcontrol.mock-support.allow_mocked_beans=true
  • deltaspike.testcontrol.mock-support.allow_mocked_producers=true

別の方法としては、META-INF/apache-deltaspike.propertiesに上記のプロパティを設定するのでも構いません。

それから必要なのはpom.xmlの設定ですが、このDynaimcMockMangerはDeltaSpike Test-Controlモジュールに含まれるので、前々回のブログと同じpom.xmlの設定で実行することができますのでそちらを参照してください。

テストの実行ログの抜粋は以下のようになります。

11 04, 2015 9:47:08 午後 org.jboss.weld.environment.se.WeldContainer initialize
INFO: WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
11 04, 2015 9:47:08 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.DynamicMockManagerTest#testMock
Mock of RdbRepository is called
11 04, 2015 9:47:08 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.DynamicMockManagerTest#testMock
11 04, 2015 9:47:08 午後 org.jboss.weld.environment.se.WeldContainer shutdown
INFO: WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down

Mockitoを使う方法

DynamicMockManagerはMockitoと組み合わせて使うこともできます。Mochitoを使うにはpom.xmlに依存ライブラリを追加してください。

		<dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

次の例では、Mochitoを使って引数10を与えた場合に戻り値として99を返すMockオブジェクトを定義しています。

	@Test
	public void testMockito() {
		MyLogic mockedLogic = mock(MyLogic.class);
		when(mockedLogic.get(10)).thenReturn(99);
		mockManager.addMock(mockedLogic);

		assertEquals(99, mylogic.get(10));
    }

DeltaSpikeにおけるMock使用上の注意

DeltaSpikeのMockを使ったテストの方法はDELTASPIKE-872に記載されているように、DeltaSpike 1.5.1では、インターセプターを使ったBeanでは適用することができないという問題が報告されています。このような制限はあるものの、単体テストでMockを使うのは非常に有効な方法だと思いましたので、このブログで紹介することにしました。

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

前回のブログに引き続きJava SE上でCDIプログラムの単体テストをする方法について書きます。今回はDeltaSpike Coreモジュールに含まれるProjectStageと@Exclude、そして最後にCdiTestRunnerと組み合わせて使う方法を紹介します。

ProjectStageとは

単体テスト、結合テスト、システムテストなど、テストのフェーズ応じてテスト用のプログラムやデータを切り替えたいということがあります。単体テストの場合はDerbyのような軽量なDBを使い、システムテストでは本番と同等の構成のDBを使うというような場合です。この切り替えのために手作業で設定ファイルを変更するのでは自動ビルドに対応するのが難しくなるので、プログラム起動のパラメーターによって切り替えられる仕組みが望まれるところです。

DeltaSpikeのProjectStageは、プロジェクトの開発ステージを実行時に指定できるようにすることで、指定されたステージに応じた処理を書けるようにするものです。ProjectStageが提供する既定義のステージとして以下が提供されています。

  • UnitTest
  • Development
  • SystemTest
  • IntegrationTest
  • Staging
  • Production

実行時にどのステージかを指定するには、次のようにJavaプログラムの起動時にシステムプロパティ(org.apache.deltaspike.ProjectStage)を設定します。

-Dorg.apache.deltaspike.ProjectStage=UnitTest

起動されたプログラム側では指定されたステージを@Injectで取得することができるので、その値にもとづいて起動のときに指定されたステージを知り、そのステージに対応した処理を書くことができます。

@Inject
ProjectStage projectStage;

// ...

if (ProjectStage.UnitTest.equals(this.projectStage)) {
    // 単体テストのステージでの処理
} else {
    // その他のステージでの処理
}

ProjectStageによる@Excludeの制御

DeltaSpikeの@Excludeは、指定されたクラスをインジェクションの対象から明示的に外すためのアノテーションです。CDI 1.1仕様では@Vetoedという同じ目的のアノテーションがありますが、@Excludeは、アノテーションの引数としてインジェクションの対象から外す条件を指定できるのが特徴です。そして、このときの条件としてProjectStageを使うことができます。

具体例を見てみましょう。MessageSenderというインタフェースを型として変数にインジェクトするシーンを考えます。

@Inject
MessageSender messageSender;

MessageSenderはメールを送信するBeanです。これを利用するBeanのテストを考えたとき、単体テストではMessageSenderにはメール送信の代わりにログを書くだけにしてもらいたいと思います。このようなときにProjectStageを使って実行時に指定されたステージに従って、単体テストのときと、それ以外のときでインジェクトされるMessageSenderを切り替えることが可能です。

次のプログラムは単体テスト以外の場合にインジェクトされるべきクラスです。@Exclude(ifProjectStage =<ステージクラス>)とすると、指定されたステージの場合にはそのクラスが排除されます。

package org.tanoseam.examples;

import javax.enterprise.context.ApplicationScoped;
import org.apache.deltaspike.core.api.exclude.Exclude;
import org.apache.deltaspike.core.api.projectstage.ProjectStage;

@ApplicationScoped
@Exclude(ifProjectStage = ProjectStage.UnitTest.class)
public class MessageSenderImpl implements MessageSender {

	public MessageSenderImpl() {}

	public void send(String message) {
		System.out.println("MessageSenderImpl: " + message);
	}
}

次に、単体テストのときにインジェクトされるクラスの定義です。指定されたステージでインジェクトされないことを設定するには@Exclude(exceptIfProjectStage = <ステージクラス>)とします。
「〜以外のステージの場合はインジェクションの対象としない」ということは、二重否定の書き方で少々わかりにくいのですが、指定されたステージではインジェクションの対象になるいうことです。

package org.tanoseam.examples;

import javax.enterprise.context.ApplicationScoped;
import org.apache.deltaspike.core.api.exclude.Exclude;
import org.apache.deltaspike.core.api.projectstage.ProjectStage;

@ApplicationScoped
@Exclude(exceptIfProjectStage = ProjectStage.UnitTest.class)
public class TestMessageSenderImpl implements MessageSender {
	
	public TestMessageSenderImpl() {}
	
	public void send(String message) {
		System.out.println("TestMessageSenderImpl: " + message);
	}
}

DeltaSpike Coreモジュールの使い方

ここまで説明してきたProjectStageと@Excludeは、JUnitのようなテスティングフレームワークとの併用が必須というわけありません。プログラム起動時にシステムプロパティでステージを指定すれば、実行時にステージ情報を取得でき、それに従って@Excludeが動作します。以下はJava SE上でWeldを使ったサンプルになります。

package org.tanoseam.examples;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.apache.deltaspike.core.api.projectstage.ProjectStage;
import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;

@ApplicationScoped
public class ProjectStageExample {
	
	public ProjectStageExample() {}

	@Inject
	ProjectStage projectStage;
	
	@Inject
    MessageSender messageSender;

	public void testSend() {
		messageSender.send("ProjectStage=" + projectStage);
    }
	
	static public void main(String args[]) {
		Weld weld = new Weld();
	    WeldContainer container = weld.initialize();
	    container.instance().select(ProjectStageExample.class).get().testSend();
	    weld.shutdown();
	}
}

ProjectStageと@Excludeは、DeltaSpike Coreモジュールに含まれます。このモジュールはMaven pom.xmlに以下の依存モジュールを設定することで有効になります。

 	<dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-api</artifactId>
            <version>${deltaspike.version}</version>
            <scope>compile</scope>
	</dependency>
		
	<dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-impl</artifactId>
            <version>${deltaspike.version}</version>
            <scope>runtime</scope>
	</dependency> 

上のプログラムではWeldをテストではなく、実行時に使っているので、Weldの依存ライブラリのスコープをcompileにしておいてください。

	<dependency>
            <groupId>org.jboss.weld.se</groupId>
            <artifactId>weld-se-core</artifactId>
            <version>${weld.version}</version>
            <scope>compile</scope>
            <!-- <scope>test</scope> -->
	</dependency>

それから、CDIを使うので前回のブログと同様にMETA-INF/beans.xmlが必要になります。忘れずにファイルを作成してください。

準備ができたらシステムプロパティorg.apache.deltaspike.ProjectStageでステージをいくつか変更して実行してみましょう。ステージにUnitTestを指定すると、messageSender変数に次のようにテスト用のMessageSenderがインジェクトされます。

$ mvn clean install exec:java -Dexec.mainClass=”org.tanoseam.examples.ProjectStageExample” -Dorg.apache.deltaspike.ProjectStage=UnitTest

INFO: WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
TestMessageSenderImpl: ProjectStage=UnitTest
11 03, 2015 1:21:10 午後 org.jboss.weld.environment.se.WeldContainer shutdown
INFO: WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down

次に、ステージをProductionへ変更すると本番用のMessageSenderがインジェクトされているのがわかります。

$ mvn clean install exec:java -Dexec.mainClass=”org.tanoseam.examples.ProjectStageExample” -Dorg.apache.deltaspike.ProjectStage=Production

11 03, 2015 1:31:31 午後 org.jboss.weld.environment.se.WeldContainer initialize
INFO: WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
MessageSenderImpl: ProjectStage=Production
11 03, 2015 1:31:31 午後 org.jboss.weld.environment.se.WeldContainer shutdown
INFO: WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down

CdiTestRunnerを使ったテストでのProjectStageの指定

最後に、JUnitのテストプログラム内でProjetStageを指定する方法について書きます。JUnit のテストでは、これまでの説明と反するように聞こえるかもしれませんが、実行時に指定されたステージによって結果が変わるのでは正しいテストを実行することができません。そこで、@TestControlというアノテーションを指定することで、テストの実行で想定されるステージをテストごとに指定します(@TestControlを指定しない場合のステージのデフォルトはUnitTestです)。以下の例はUnitTestのステージを指定したときのサンプルになります。

package org.tanoseam.examples;

import javax.inject.Inject;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.apache.deltaspike.core.api.projectstage.ProjectStage;
import org.apache.deltaspike.testcontrol.api.TestControl;
import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner;

@RunWith(CdiTestRunner.class)
@TestControl(projectStage = ProjectStage.UnitTest.class)
public class ProjectStageTest {

	@Inject
	ProjectStage projectStage;
	
	@Inject
    MessageSender messageSender;

	@Test
	public void testSend() {
		messageSender.send("ProjectStage=" + projectStage);
    }
}

今度はCdiTestRunnerを使っているのでWeldの起動・停止のコードを書く必要がなく、 mvn test だけで実行できます。
全体的にテストコードがすっきりしましたね。

1 03, 2015 7:34:41 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.ProjectStageTest#testSend
11 03, 2015 7:34:41 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.ProjectStageTest#testSend
TestMessageSenderImpl: ProjectStage=UnitTest
11 03, 2015 7:34:41 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.ProjectStageTest#testSend
11 03, 2015 7:34:41 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.ProjectStageTest#testSend

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モジュールについて紹介する予定です。

 

Apache DeltaSpike 1.0

待望のApacheDeltaSpike 1.0が6月14日にリリースされました。2011年の12月にincubatorプロジェクトとして開始してから、約2年半の歳月を経て、バージョン1.0がようやくリリースされたのです(DeltaSpikeプロジェクトの誕生の経緯はこちらを参照ください)。言うまでもなく、1.0というバージョンはプロジェクトにとって多くのユーザが使いはじめる重要なバージョンになります(このブログを公開した時点ではすでにバージョン1.0.1がリリースされていますので、これからダウンロードする方は最新版を使ってくださいね)。

DeltaSpikeの機能概要

DeltaSpike 1.0が提供する機能は3つに分類できます。

  • Java EE以外の環境に提供するJava EE 7ライクな機能 (表1)
  • JSFをより使いやすくするための機能 (表2)
  • アプリケーション開発の生産性を改善する機能 (表3)
表1:非Java EE環境に提供するJava EE 7 機能
機能 概要
@Transactionalアノテーション EJB以外のBeanでトランザクションを制御可能にする
インジェクション可能なサーブレットオブジェクト Java EE 6/CDI 1.0環境でCDI Beanをサーブレットにインジェクションできる
インジェクション可能なリソース コンフィギュレーションやリソースバンドルを簡単にインジェクトできる
@Excludeアノテーション CDIの有効な環境でもCDIの処理の対象にならないようにする
スケジューリングタスク 非Java EE 7環境で実行可能な非同期プロセス
Beanバリデーション統合 CDI BeanIとEJBバリデーションをConstraint-Validatorにインジェクトできる
BeanProvider JPA-2.0 EntityListenersやSpring BeansBeanManagerのような非管理クラスにおいて CDI beansにアクセスできる
表2:JSFの改善機能
機能 概要
マルチウィンドウ処理  バッチのための論理的なウィンドウを管理できるようにする。JSFの場合はブラウザタブを独立して扱えるようになる。
タイプセーフ view-config (セキュリティなどの) メタデータをビューに柔軟に結びつけることを可能にする。JSFの場合はより頑丈なナビゲーションを提供し、メンテナンスフェーズの助けとなる。
View-Controller タイプセーフ view-configをベースとして、view-controllerアノテーションは標準タグのタイプセーフな代替を提供する。
コンバータとバリデータに関するインジェクション CDI BeanとEJZBをJSFコンバータとバリデータへインジェクト可能にする。
CDIへのJSFイベントブロードキャスト JSFイベントに関する通知をCDIで受けられるようにする。
表3:生産性の改善機能
機能 概要
アノテーションベースのセキュリティ 頑強で、使えて、アプリケーションに侵入しない(non invasive) なセキュリティソリューションを構築する基盤※ non invasive(非侵襲的)とはアプリのコードにフレームワークやコンテナ依存のコードが入り込まないような方法
新しい CDI スコープ TransactionScoped, WindowScoped, ViewScoped, ViewAccess scope, Grouped conversion scope
コンテナ制御とテスト制御 Java SEでCDIが使える。開始、停止、実行中のCDIコンテナへのクラスの追加などの統一されたAPIを提供。
データモジュール エンティティ・フレームワークのソリューション。JDBCとともに、コンテナ管理またはアプリケーション管理の永続コンテキストをサポートする。
分離された例外処理 CDIオブザーバと同様に例外処理を一箇所で実行できるようにする。
JMXの統合 任意のCDI Beanが1つのアノテーションによって簡単にJMXで公開できる。
タイプセーフi18n メッセージ インタフェースやリソースバンドルと一緒にローカライズされたメッセージを簡単に使える。メッセージがソースコードという文脈の中でわかりやすくなる。
タイプセーフProject-Stages JSFでのProject stageと比べて、DeltaSpikeはタイプセーフであるにもかかわらず拡張可能な方法を提供できるし、CDIベースのアプリケーションで使うことができる。

CDI仕様は、拡張可能なコンテキスト(スコープの実装)とタイプセーフなインジェクションを提供します。DeltaSpikeが提供する機能には、「新しいスコープ」、「タイプセーフ」、「インジェクション」という言葉が目立つのはそのような理由からです。DeltaSpikeはこれらのCDIのコンセプトを使ったユーティリティやライブラリ機能をCDIコンテナ上で動作するポータブルなCDI拡張 (Extension) として提供します。

新しいJava EE はCDIをベースにサービスが統合され、サービス間で連携します。CDIは単なるインジェクションのフレームワークではなく、Java EEの背骨に相当するコアな仕様です。古いJava EEではEJBやJMSなどの仕様ごとに縦割りでアノテーションが決められてきました。CDIはJava EEを構成するサービスを横方向に連携させるものです。

将来Java EEが提供する新しいサービスとユーザによってCDIで作られたサービスは、同じCDI上のサービスという点では違いが無くなっていくことでしょう。私は、CDIの登場によって、Java EEが閉じたJava EEから、拡張可能な開かれたJava EEになったと考えています。DeltaSpikeの提供する機能は、Java EEに取り込むことでJava EEの機能をコミュニティの力によって拡張するものです。足りないものがあれば作れば良い。そこに魅力を感じます。

 

%d人のブロガーが「いいね」をつけました。