Java EEでのテスト、といえばArquillianのイメージがありますが、もうちょっと軽いものはないかなぁと思っていたところ、そういえばApache DeltaSpikeにテスト用のモジュールがあったのを思い出しました。
Apache DeltaSpikeってなんなの?という話ですが、CDIの拡張モジュールらしいです。
JBoss SeamとApache DeltaSpikeの今後
こちらのスライドでも、名前がちょっと出ていますね。
で、今回はテスト用のモジュールを試してみます。
というか、CDIを使ったクラスをJava EEコンテナなしでテストコードで動かしたい、と。
こちらに、動かしてみたというエントリもありました。
CDIを使ったプログラムの単体テスト(1) | なるほど!ザ・Weld
CDIを使ったプログラムの単体テスト(2) | なるほど!ザ・Weld
CDIを使ったプログラムの単体テスト(3) | なるほど!ザ・Weld
いくつか動かしてみたいところはあるのですが、まずは初歩的なパターンということで。
準備
それでは、Apache DeltaSpikeを使うためのコードを用意してみます。
基本的には、ドキュメントのこちらのページを参考に用意します。
CDIの実装は、Weldとします。
で、Scalaで書きます、と。
build.sbt
name := "cdi-testing-with-deltaspike" version := "0.0.1-SNAPSHOT" organization := "org.littlewings" scalaVersion := "2.11.8" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) enablePlugins(JettyPlugin) webappWebInfClasses := true artifactName := { (scalaVersion: ScalaVersion, module: ModuleID, artifact: Artifact) => artifact.name + "." + artifact.extension } fork in Test := true // 依存関係
一応、xsbt-web-pluginを使ってWARのプロジェクトにしています。
project/plugins.sbt
logLevel := Level.Warn addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
libraryDependencies ++= Seq( "javax" % "javaee-web-api" % "7.0" % Provided,
ドキュメントに則り、Apache DeltaSpikeのテスト用のモジュール。
"org.apache.deltaspike.modules" % "deltaspike-test-control-module-api" % "1.7.1" % Test, "org.apache.deltaspike.modules" % "deltaspike-test-control-module-impl" % "1.7.1" % Test,
また、ドキュメントには書いてありませんが、通常必要になると思うのでCoreについても依存関係を足しておきます。
"org.apache.deltaspike.core" % "deltaspike-core-api" % "1.7.1" % Compile, "org.apache.deltaspike.core" % "deltaspike-core-impl" % "1.7.1" % Runtime,
CDIコンテナの起動停止のためのモジュール。Weldを使用するので専用のモジュールと、weld-se-coreを追加します。
"org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-weld" % "1.7.1" % Test, "org.jboss.weld.se" % "weld-se-core" % "2.3.5.Final" % Test,
weld-seでも、weld-servletでもないんですねー。これで、テスト実行時にWeldの起動、停止をしてくれるみたいです。
あとは、sbtでScala+JUnitを動かすための依存関係。
"org.scalatest" %% "scalatest" % "2.2.6" % Test, "junit" % "junit" % "4.12" % Test, "com.novocode" % "junit-interface" % "0.11" % Test )
こんなところです。
あと、beans.xmlを用意しておきます。
src/main/resources/META-INF/beans.xml
<?xml version="1.0" encoding="UTF-8"?> <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" bean-discovery-mode="annotated"> </beans>
試してみる
では、ここからはいくつかのバリエーションでApache DeltaSpikeを使ったCDIのテストを動かしてみます。テストといっても、とりあえず動作させてみるだけですが。
以下のようなことをやってみます。
- ApplicationScoped、SessionScoped、RequestScoped、DependentなCDI管理Beanを定義して確認
- CDI管理Beanに、さらにCDI管理Beanを@Injectして確認
- CDI管理Beanに、Interceptorを有効化して確認
それぞれ、やってみます。
ApplicationScoped、SessionScoped、RequestScoped、DependentなCDI管理Beanを定義して確認
まずは、各スコープのCDI管理を定義してみます。
src/main/scala/org/littlewings/javaee7/cdi/ScopedService.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.{ApplicationScoped, Dependent, RequestScoped, SessionScoped} @ApplicationScoped class ApplicationScopedService { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } @SessionScoped @SerialVersionUID(1L) class SessionScopedService extends Serializable { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } @RequestScoped class RequestScopedService { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } @Dependent @SerialVersionUID(1L) class DependentService extends Serializable { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") }
単純なものですね。
では、テストコードを書いてみます。
src/test/scala/org/littlewings/javaee7/cdi/CdiDeltaspikeTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.junit.Test import org.junit.runner.RunWith import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CdiDeltaspikeTest extends JUnitSuite { @Inject private var applicationScopedService: ApplicationScopedService = _ @Inject private var sessionScopedService: SessionScopedService = _ @Inject private var requestScopedService: RequestScopedService = _ @Inject private var dependentService: DependentService = _ @Test def runTest1(): Unit = { println("===== runTest1 =====") applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println() } @Test def runTest2(): Unit = { println("===== runTest2 =====") applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println() } }
テストというより、printlnしているだけですが…。
このクラスの特徴は、@RunWithアノテーションにCdiTestRunnerクラスを指定しているところですね。
@RunWith(classOf[CdiTestRunner]) class CdiDeltaspikeTest extends JUnitSuite {
このクラスを使うことで、SessionScopedやRequestScopedも使えるようになるみたいです。
実行結果。
===== runTest1 ===== Hello, ApplicationScopedService#1341706533 Hello, SessionScopedService#1329315688 Hello, RequestScopedService#1541697437 Hello, DependentService#1066513687 ===== runTest2 ===== Hello, ApplicationScopedService#1341706533 Hello, SessionScopedService#37887172 Hello, RequestScopedService#818493590 Hello, DependentService#1503660218
インスタンスのハッシュコードから、ApplicationScopedのCDI管理Beanは同じものが使いまわされているようですが、それ以外についてはインスタンスが都度作成されているようですね。まあ、Dependentはちょっと事情が違うでしょうけれど。
CDI管理Beanに、さらにCDI管理Beanを@Injectして確認
続いて、CDI管理Beanに、別のCDI管理Beanを@Injectして使ってみます。
先ほど作成したCDI管理Beanを、@Injectして使うクラスを定義。
src/main/scala/org/littlewings/javaee7/cdi/InjectedService.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.{ApplicationScoped, Dependent, RequestScoped, SessionScoped} import javax.inject.Inject @ApplicationScoped class InjectedApplicationScopedService { @Inject private var applicationScopedService: ApplicationScopedService = _ @Inject private var sessionScopedService: SessionScopedService = _ @Inject private var requestScopedService: RequestScopedService = _ @Inject private var dependentService: DependentService = _ def say(): Unit = { applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } } @SessionScoped @SerialVersionUID(1L) class InjectedSessionScopedService extends Serializable { @Inject private var applicationScopedService: ApplicationScopedService = _ @Inject private var sessionScopedService: SessionScopedService = _ @Inject private var requestScopedService: RequestScopedService = _ @Inject private var dependentService: DependentService = _ def say(): Unit = { applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } } @RequestScoped class InjectedRequestScopedService { @Inject private var applicationScopedService: ApplicationScopedService = _ @Inject private var sessionScopedService: SessionScopedService = _ @Inject private var requestScopedService: RequestScopedService = _ @Inject private var dependentService: DependentService = _ def say(): Unit = { applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } } @Dependent @SerialVersionUID(1L) class InjectedDependentService extends Serializable { @Inject private var applicationScopedService: ApplicationScopedService = _ @Inject private var sessionScopedService: SessionScopedService = _ @Inject private var requestScopedService: RequestScopedService = _ @Inject private var dependentService: DependentService = _ def say(): Unit = { applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } }
テストコードは、こちら。
src/test/scala/org/littlewings/javaee7/cdi/CdiDeltaspikeInjectedTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.junit.Test import org.junit.runner.RunWith import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CdiDeltaspikeInjectedTest extends JUnitSuite { @Inject private var applicationScopedService: InjectedApplicationScopedService = _ @Inject private var sessionScopedService: InjectedSessionScopedService = _ @Inject private var requestScopedService: InjectedRequestScopedService = _ @Inject private var dependentService: InjectedDependentService = _ @Test def runTest1(): Unit = { println("===== runTest1 =====") applicationScopedService.say() println() sessionScopedService.say() println() requestScopedService.say() println() dependentService.say() println() } @Test def runTest2(): Unit = { println("===== runTest2 =====") applicationScopedService.say() println() sessionScopedService.say() println() requestScopedService.say() println() dependentService.say() println() } }
実行結果。
===== runTest1 ===== Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#1752182275 Hello, RequestScopedService#2049051802 Hello, DependentService#37887172 Hello, InjectedApplicationScopedService#818493590 Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#1752182275 Hello, RequestScopedService#2049051802 Hello, DependentService#1503660218 Hello, InjectedSessionScopedService#864852424 Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#1752182275 Hello, RequestScopedService#2049051802 Hello, DependentService#565839681 Hello, InjectedRequestScopedService#1489933928 Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#1752182275 Hello, RequestScopedService#2049051802 Hello, DependentService#24293395 Hello, InjectedDependentService#1449605932 ===== runTest2 ===== Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#530696881 Hello, RequestScopedService#1894788146 Hello, DependentService#37887172 Hello, InjectedApplicationScopedService#818493590 Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#530696881 Hello, RequestScopedService#1894788146 Hello, DependentService#1644236636 Hello, InjectedSessionScopedService#1375394559 Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#530696881 Hello, RequestScopedService#1894788146 Hello, DependentService#1075803699 Hello, InjectedRequestScopedService#838812606 Hello, ApplicationScopedService#1706099897 Hello, SessionScopedService#530696881 Hello, RequestScopedService#1894788146 Hello, DependentService#1533985074 Hello, InjectedDependentService#1548010882
ApplicationScopedなCDI管理Beanについては、どのテストケースにおいても同じインスタンスが使われています。また、SessionScopedやRequestScopedなCDI管理Beanについては、テストケースをまたぐと別のインスタンスになりますが、同一のテストケース内であれば同じインスタンスが使いまわされているようですね。
DependentなCDI管理Beanについては、ApplicationScopedなものに@Injectされたものについては、テストケースが変わっても同じインスタンスが使われる、と。それ以外のスコープのCDI管理Beanに@Injectされる時は、都度違うインスタンスになっていますね。
なるほど。
CDI管理Beanに、Interceptorを有効化して確認
最後は、Interceptorを付けて確認してみます。トレースログ出力Interceptorを作ってみましょう。
アノテーション。ここだけ、Javaです…。
src/main/java/org/littlewings/javaee7/cdi/EnableTracing.java
package org.littlewings.javaee7.cdi; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.interceptor.InterceptorBinding; @Inherited @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface EnableTracing { }
Interceptor。
src/main/scala/org/littlewings/javaee7/cdi/TraceInterceptor.scala
package org.littlewings.javaee7.cdi import javax.annotation.Priority import javax.interceptor.{AroundInvoke, Interceptor, InvocationContext} @Interceptor @Priority(Interceptor.Priority.APPLICATION) @EnableTracing @SerialVersionUID(1L) class TraceInterceptor extends Serializable { @AroundInvoke def trace(ic: InvocationContext): Any = { val targetClass: Class[_] = ic.getTarget.getClass.getSuperclass val method = ic.getMethod println(s"[start] ${targetClass.getSimpleName}#${method.getName}") val result = ic.proceed() println(s"[ end ] ${targetClass.getSimpleName}#${method.getName}") result } }
Interceptorを適用した、CDI管理Bean。
src/main/scala/org/littlewings/javaee7/cdi/TracingService.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.{ApplicationScoped, Dependent, RequestScoped, SessionScoped} @EnableTracing @ApplicationScoped class TracingApplicationScopedService { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } @EnableTracing @SessionScoped @SerialVersionUID(1L) class TracingSessionScopedService extends Serializable { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } @EnableTracing @RequestScoped class TracingRequestScopedService { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") } @EnableTracing @Dependent @SerialVersionUID(1L) class TracingDependentService extends Serializable { def say(): Unit = println(s"Hello, ${getClass.getSimpleName}#${hashCode}") }
テスト。
src/test/scala/org/littlewings/javaee7/cdi/CdiDeltaspikeWithInterceptorTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.junit.Test import org.junit.runner.RunWith import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CdiDeltaspikeWithInterceptorTest extends JUnitSuite { @Inject private var applicationScopedService: TracingApplicationScopedService = _ @Inject private var sessionScopedService: TracingSessionScopedService = _ @Inject private var requestScopedService: TracingRequestScopedService = _ @Inject private var dependentService: TracingDependentService = _ @Test def runTest1(): Unit = { println("===== runTest1 =====") applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println() } @Test def runTest2(): Unit = { println("===== runTest2 =====") applicationScopedService.say() sessionScopedService.say() requestScopedService.say() dependentService.say() println() } }
結果。
===== runTest1 ===== [start] TracingApplicationScopedService#say Hello, TracingApplicationScopedService$Proxy$_$$_WeldSubclass#1075803699 [ end ] TracingApplicationScopedService#say [start] TracingSessionScopedService#say Hello, TracingSessionScopedService$Proxy$_$$_WeldSubclass#2065718717 [ end ] TracingSessionScopedService#say [start] TracingRequestScopedService#say Hello, TracingRequestScopedService$Proxy$_$$_WeldSubclass#275563320 [ end ] TracingRequestScopedService#say [start] TracingDependentService#say Hello, TracingDependentService$Proxy$_$$_WeldSubclass#2053628870 [ end ] TracingDependentService#say ===== runTest2 ===== [start] TracingApplicationScopedService#say Hello, TracingApplicationScopedService$Proxy$_$$_WeldSubclass#1075803699 [ end ] TracingApplicationScopedService#say [start] TracingSessionScopedService#say Hello, TracingSessionScopedService$Proxy$_$$_WeldSubclass#1676827075 [ end ] TracingSessionScopedService#say [start] TracingRequestScopedService#say Hello, TracingRequestScopedService$Proxy$_$$_WeldSubclass#651100072 [ end ] TracingRequestScopedService#say [start] TracingDependentService#say Hello, TracingDependentService$Proxy$_$$_WeldSubclass#1611370719 [ end ] TracingDependentService#say
ちゃんと、Interceptorも効いていますね。
まとめ
Apache DeltaSpikeのTest-Control(テスト向けモジュール)を使って、CDI管理BeanをCDIコンテナを使って動かしてみました。
ふつうに動いてそうですし、Interceptorも効いていてよいですね。あとは、モックとか@Transactionalはどうしようというところですが…ドキュメントに続きがあるところは、また追ってみたいと思います。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-testing-with-deltaspike