CLOVER🍀

That was when it all began.

Apache DeltaSpikeでCDIのテストを動かしてみる

Java EEでのテスト、といえばArquillianのイメージがありますが、もうちょっと軽いものはないかなぁと思っていたところ、そういえばApache DeltaSpikeにテスト用のモジュールがあったのを思い出しました。

Test-Control Module

Apache DeltaSpikeってなんなの?という話ですが、CDIの拡張モジュールらしいです。

JBoss SeamとApache DeltaSpikeの今後

こちらのスライドでも、名前がちょっと出ていますね。


で、今回はテスト用のモジュールを試してみます。

というか、CDIを使ったクラスをJava EEコンテナなしでテストコードで動かしたい、と。

こちらに、動かしてみたというエントリもありました。

CDIを使ったプログラムの単体テスト(1) | なるほど!ザ・Weld

CDIを使ったプログラムの単体テスト(2) | なるほど!ザ・Weld

CDIを使ったプログラムの単体テスト(3) | なるほど!ザ・Weld

いくつか動かしてみたいところはあるのですが、まずは初歩的なパターンということで。

準備

それでは、Apache DeltaSpikeを使うためのコードを用意してみます。

基本的には、ドキュメントのこちらのページを参考に用意します。

Test-Control Module

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")

ライブラリの依存関係は、まずはJava EEのAPI。

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も使えるようになるみたいです。

https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/modules/test-control/api/src/main/java/org/apache/deltaspike/testcontrol/api/junit/CdiTestRunner.java

実行結果。

===== 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はちょっと事情が違うでしょうけれど。

とにかく、Java SEでのJUnitテストでも、CDIを使ったテストコードを動かすことができました、と。

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