Spring TestでJBoss Embeddedサーバーを利用するための手順

Seamの場合最初からSeamTestというフレームワークが付属しており、Embedded JBossサーバーを使ってJBossの機能をTestNGのテストクラスから実行できます。同様にして、Spring Frameworkに付属しているSpring Testを利用して、Springベースのアプリケーションで自動化された結合試験を実行する際にEmbedded JBossサーバを利用するための手順を以下に示します。

前提知識 Spring Testについて

Spring TestについてはSpring参照マニュアルの9章を参照してください。
9. Testing
このフレームワークを利用するとSpringのDIコンテナーを使ったJUnitTestNGの単体試験を容易に作成することができます。ここではSpring Test自体の使い方については前提知識として説明しません。(とは言ってみたもののSpring Testの新しいバージョンに関する日本語のまとまった情報は今のところあまりないようですね。時間があったら別の機会に基本的な使い方についてまとめてみたいと思いますが。)

依存するライブラリーの設定

JBoss EmbeddedサーバーとSpring Testを利用するために最低限必要なライブラリーの依存関係を追加します。たとえば、Mavenを使った場合、以下をpomに追加します。(JBossの新しいMavenレポジトリー - 達人プログラマーを目指してで書いたようにjbossのレポジトリーを登録しておく必要があります。)

	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-test</artifactId>
		<version>${spring.version}</version>
		<scope>test</scope>
	</dependency>

	<dependency>
		<groupId>org.jboss.embedded</groupId>
		<artifactId>jboss-embedded-all</artifactId>
		<version>beta3.SP12</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.jboss.embedded</groupId>
		<artifactId>jboss-embedded</artifactId>
		<version>beta3.SP12</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.jboss.embedded</groupId>
		<artifactId>thirdparty-all</artifactId>
		<version>beta3.SP12</version>
		<scope>test</scope>
	</dependency>
	<!--
	hibernate-all.jarにアノテーションやJPAを含めたHibernate関連のライブラリがすべて含まれる
	ため別途hibernateの依存関係を追加している場合はその依存関係をあらかじめ削除する必要がある
	-->
	<dependency>
		<groupId>org.jboss.embedded</groupId>
		<artifactId>hibernate-all</artifactId>
		<version>beta3.SP12</version>
		<scope>test</scope>
	</dependency>

JBoss Embeddedのダウンロードとインストール

jarファイルとしての依存関係とは別に、以下からJBoss Embeddedをダウンロードします。
http://repository.jboss.org/maven2/org/jboss/embedded/jboss-embedded/beta3.SP12/jboss-embedded-beta3.SP12-bin.zip
ダウンロードしたら、zipファイルを展開した中のbootstrapフォルダーの中身(bootstrapフォルダー自身は含まない)をsrc/test/resource配下にまるごとコピーします。以下のようになっているはずです。

VMオプションの指定(Java6の場合)

Java6の場合テスト実行時のVM起動時のオプションに以下を追加する必要があります。

-Dsun.lang.ClassLoader.allowArraySyntax=true

eclipseの場合、テストクラスごとに、毎回指定するのが面倒であれば、設定ダイアログを開きJava⇒インストール済みのJREを選択し、編集ボタンをクリックして表示される「JREの定義ダイアログ」でデフォルトのVM引数を編集できます。

JBoss起動を行うContextLoaderの実装クラスの作成

以上でEmbedded JBossサーバーを起動する準備は整ったのですが、問題はサーバーを起動するタイミングです。通常結合テストの場合SpringのコンテキストでJTAやJMSのサービスにアクセスさせる必要があるため、DIコンテナーの起動前にサーバーを起動する必要があります。したがって、通常の@Beforeメソッド内でJBossを起動するのでは手遅れです。この問題を解決するためには、以下のようなContextLoaderの実装クラスを作成します。

import org.jboss.deployers.spi.DeploymentException;
import org.jboss.embedded.Bootstrap;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.support.GenericXmlContextLoader;

public class JBossBootstrappingXmlContextLoader extends GenericXmlContextLoader {

	@Override
	protected void prepareContext(GenericApplicationContext context) {
		super.prepareContext(context);
		
		try {
			if (!Bootstrap.getInstance().isStarted()) {
				Bootstrap bootstrap = Bootstrap.getInstance();
				bootstrap.bootstrap();
			}
		} catch (DeploymentException ex) {
			throw new RuntimeException("埋め込みJBossの起動に失敗しました。", ex);
		}
	}
}

あとは以下のように@ContextConfigurationのloader属性にて先ほど作成したクラスを指定してやります。

@ContextConfiguration(loader=JBossBootstrappingXmlContextLoader.class)
public class BatchJobLauncherTest extends AbstractJUnit4SpringContextTests {

これで、JTAやJMSなどJBossの機能を使った結合試験の自動化が実現できます。最初もっと大変かなと思いましたが、意外に簡単に設定できました。特に、EJBを使わないSpringアプリケーションであれば、わざわざデプロイも不要でDIコンテナーさえ起動すればよいので、思ったより軽量でした。

結合テスト専用プロジェクトの分離について

以上のようにEmbedded JBossとSpring Testを組み合わせることで、JBoss + Springのアプリケーションの自動化された結合試験を作成することができます。ただし、上記のように大量のjarファイルを依存させる必要もありますし、普通のJUnitの単体試験と比べると実行にも時間がかかります。よって、ベストプラクティスとしては結合試験専用のプロジェクトを別プロジェクトとして作成するのがよいと思います。

参考サイト

以下を参考にさせていただきました。
JBoss Embedded and Maven « Paul’s Weblog
http://myblog.shriharisc.com/2010/06/21/using-embedded-jboss-for-functional-flow-testing-for-ejb3/
(追記)
EclipseWTPの設定でJBossサーバーランタイムがライブラリーとしてクラスパスに追加されていると、以下の例外が発生するようです。この場合、プロジェクトのビルドの設定でJBossサーバーランタイムをクラスパスから除外する必要があります。

ERROR [org.jboss.kernel.plugins.dependency.AbstractKernelController] Error installing to Start: name=JMXKernel state=Create
java.lang.NoSuchMethodError: org.jboss.system.ServiceController.setKernel(Lorg/jboss/kernel/Kernel;)V
        at org.jboss.embedded.adapters.JMXKernel.start(JMXKernel.java:164)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:585)
        at org.jboss.reflect.plugins.introspection.ReflectionUtils.invoke(ReflectionUtils.java:56)
        at org.jboss.reflect.plugins.introspection.ReflectMethodInfoImpl.invoke(ReflectMethodInfoImpl.java:110)
        at org.jboss.joinpoint.plugins.BasicMethodJoinPoint.dispatch(BasicMethodJoinPoint.java:66)
        at org.jboss.kernel.plugins.dependency.KernelControllerContextAction$JoinpointDispatchWrapper.execute(KernelControllerContextAction.java:214)
        at org.jboss.kernel.plugins.dependency.ExecutionWrapper.execute(ExecutionWrapper.java:45)
        at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.dispatchExecutionWrapper(KernelControllerContextAction.java:108)
        at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.dispatchJoinPoint(KernelControllerContextAction.java:69)
        at org.jboss.kernel.plugins.dependency.LifecycleAction.installActionInternal(LifecycleAction.java:221)
        at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.installAction(KernelControllerContextAction.java:135)
        at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.installAction(KernelControllerContextAction.java:46)
        at org.jboss.dependency.plugins.action.SimpleControllerContextAction.simpleInstallAction(SimpleControllerContextAction.java:62)
        at org.jboss.dependency.plugins.action.AccessControllerContextAction.install(AccessControllerContextAction.java:71)
        at org.jboss.dependency.plugins.AbstractControllerContextActions.install(AbstractControllerContextActions.java:51)
        at org.jboss.dependency.plugins.AbstractControllerContext.install(AbstractControllerContext.java:327)
        at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:1309)
        at org.jboss.dependency.plugins.AbstractController.incrementState(AbstractController.java:734)
        at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:862)
        at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:784)
        at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:574)
        at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:398)
        at org.jboss.kernel.plugins.deployment.AbstractKernelDeployer.deployBean(AbstractKernelDeployer.java:309)
        at org.jboss.kernel.plugins.deployment.AbstractKernelDeployer.deployBeans(AbstractKernelDeployer.java:279)
        at org.jboss.kernel.plugins.deployment.AbstractKernelDeployer.deploy(AbstractKernelDeployer.java:130)
        at org.jboss.kernel.plugins.deployment.xml.BeanXMLDeployer.deploy(BeanXMLDeployer.java:96)
        at org.jboss.embedded.Bootstrap.deployBaseBootstrapUrl(Bootstrap.java:130)
        at org.jboss.embedded.Bootstrap.bootstrapURL(Bootstrap.java:142)
        at org.jboss.embedded.Bootstrap.bootstrap(Bootstrap.java:183)
        at org.jboss.embedded.Bootstrap.bootstrap(Bootstrap.java:195)
        at org.jboss.seam.mock.EmbeddedBootstrap.startAndDeployResources(EmbeddedBootstrap.java:11)
        at org.jboss.seam.mock.AbstractSeamTest.startJbossEmbeddedIfNecessary(AbstractSeamTest.java:1024)
        at org.jboss.seam.mock.AbstractSeamTest.startSeam(AbstractSeamTest.java:915)
        at org.jboss.seam.mock.SeamTest.startSeam(SeamTest.java:58)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:585)
        at org.testng.internal.MethodHelper.invokeMethod(MethodHelper.java:580)
        at org.testng.internal.Invoker.invokeConfigurationMethod(Invoker.java:398)
        at org.testng.internal.Invoker.invokeConfigurations(Invoker.java:145)
        at org.testng.internal.Invoker.invokeConfigurations(Invoker.java:82)
        at org.testng.SuiteRunner.privateRun(SuiteRunner.java:278)
        at org.testng.SuiteRunner.run(SuiteRunner.java:198)
        at org.testng.TestNG.createAndRunSuiteRunners(TestNG.java:823)
        at org.testng.TestNG.runSuitesLocally(TestNG.java:790)
        at org.testng.TestNG.run(TestNG.java:708)
        at org.testng.remote.RemoteTestNG.run(RemoteTestNG.java:73)
        at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:124)