PartialBeanによるタイプセーフなハンドラー実装

DeltaSpike PartialBeanモジュールはインタフェース(または、抽象クラス)とハンドラーをバインドする仕組みを提供します。PartialBeanを使うとBeanの実装クラスを(Reflectionでお馴染みの)InvocationHandlerとして実装することができます。Partial(部分的な、不完全な)というのは、インタフェースを実装すればハンドラーが実装を提供するので、開発者がBeanの完全な実装をしなくとも良いという意味でしょう。

InvocationHandlerというと、それを聞いた人はきっとJavaのDynamic Proxyを想像されると思います。Dynamic ProxyではProxyへの呼び出しはターゲットのオブジェクトを呼び出します。しかし、このPartialBeanは実装上の仕組みとしてはDynamic Proxyを使ってはいるものの、ターゲットのBeanが別に存在するのではなく、ハンドラー自身がCDI Beanとして扱われます。ここが重要です。PartialBeanはCDIのインタセプターとは異なり、Beanの呼び出しに割り込んで何かを処理するものではなく、Beanそのものとしてインジェクションの対象になるのです。

そこで、PartialBeanではインタフェース部分の定義が重要になります。PartialBeanでは、インタフェースにおけるメソッド定義やメソッドに付加されたアノテーションの情報に基づいてハンドラーが処理を実行します。例えば、DeltaSpike DataモジュールではQueryの定義の実装にPartialBeanを使用しています。クエリの式をメソッドのアノテーションとして提供するのです。

@Query("select p from Person p where p.ssn = ?1")
Person findBySSN(String ssn);

これが意味することをよく考えてみましょう。ハンドラーはアノテーションの情報を使ってクエリを実行することができます。開発者はインタフェースからSQL式を理解することができ、しかもクエリを実行するときはメソッドを実行すれば良いのです。メソッドは引数の型や戻り値の型を提供しますから、呼び出し側が間違った呼び出しをしてもコンパイル時に型の不整合を検出することができます。つまり、タイプセーフです。PartialBeanは文字列の世界とタイプセーフな世界をつなぐ面白い仕組みと言えるでしょう。

PartialBeanのセットアップ

PartialBeanモジュールを使うにはMaven pom.xmlの依存ライブラリとして以下の設定を追加します。

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

@PartialBeanBindingの定義

インタフェースとハンドラーを結びつけるためには@PartialBeanBinding使って定義されたアノテーションが必要です。この例では@MyPartialBeanBindingというアノテーションを定義しています。

package org.tanoseam.examples;

import org.apache.deltaspike.partialbean.api.PartialBeanBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@PartialBeanBinding
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyPartialBeanBinding {}

インタフェース実装の例

次はPartialBeanのインタフェースの例です。@MyPartialBeanBindingというアノテーションをつけているので、これがPartialBeanであることがわかります。また、メソッド定義に@ValueHolderというアノテーションが付いているのに注意してください。このアノテーションは、後で、ハンドラー実装で参照します。

package org.tanoseam.examples;

import javax.enterprise.context.Dependent;

@Dependent
@MyPartialBeanBinding
public interface MyPartialBean {
    @ValueHolder(value="PartialBean")
    String getValue();
}

ハンドラーの実装

PartialBeanのハンドラーはInvocationHandlerのInvokeメソッドを実装します。
このサンプルプログラムでは、メソッドに付加されたアノテーションからvalueを取得しています。

package org.tanoseam.examples;

import java.lang.reflect.Method;
import javax.enterprise.context.Dependent;

@Dependent
@MyPartialBeanBinding
public class MyPartialBeanHandler implements java.lang.reflect.InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ValueHolder holder = method.getAnnotation(ValueHolder.class);
        String value = holder.value();
        return value;
    }
}

テストコード

PartialBeanの使用方法は簡単で、通常のBeanと同様に@Injectで変数に設定することができます。

package org.tanoseam.examples;

import static org.junit.Assert.assertEquals;

import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;

@RunWith(CdiTestRunner.class)
public class PartialBeanTest {

    @Inject
    private MyPartialBean bean;

    @Test
    public void testPartialBean() {
          assertEquals("PartialBean", bean.getValue());
    }
}

単体テストのための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
%d人のブロガーが「いいね」をつけました。