Apache DeltaSpikeのテスト用モジュールには、モックを使ったテストができる機能があります。
インターセプターがかかったCDI管理Beanはモック化できないという制限はあるようですが、ちょっと試してみましょう。
Attention: Mocking CDI beans is not supported for every feature of CDI and/or every implementation version. For example, we can not mock intercepted CDI beans and with some implementations mocking specialized beans fails. Usually all features are active by default, however, due to those reasons we deactivated this feature by default. You can enable it by adding
https://deltaspike.apache.org/documentation/test-control.html#MockFrameworks
なお、Mockitoとも合わせて使えるようです。
参考)
CDIを使ったプログラムの単体テスト(3) | なるほど!ザ・Weld
実際にモックを使うには、DynamicMockManagerというクラスを使用します。
準備
まずはプロジェクトの準備。
なんとなく、WARのプロジェクトとして定義しています。
build.sbt
name := "cdi-testing-deltaspike-with-mock" 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 libraryDependencies ++= Seq( "javax" % "javaee-web-api" % "7.0" % Provided, "org.apache.deltaspike.core" % "deltaspike-core-api" % "1.7.1" % Test, "org.apache.deltaspike.core" % "deltaspike-core-impl" % "1.7.1" % Test, "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, "org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-weld" % "1.7.1" % Test, "org.jboss.weld.se" % "weld-se-core" % "2.3.5.Final" % Test, "org.mockito" % "mockito-all" % "1.10.19" % Test, "org.scalatest" %% "scalatest" % "2.2.6" % Test, "junit" % "junit" % "4.12" % Test, "com.novocode" % "junit-interface" % "0.11" % Test )
deltaspike-core-api、deltaspike-core-implは入れておく必要があります(後述)。
あと、Mockitoはなくてもいいのですが、今回はMockitoも使うパターンも試すので、合わせて入れてあります。
xsbt-web-pluginの有効化。
project/plugins.sbt
logLevel := Level.Warn addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
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>
とりあえず、準備はここまでです。
テスト対象のCDI管理Bean
では、テスト対象となるCDI管理Beanを作成しましょう。
このような単純なものを用意。
src/main/scala/org/littlewings/javaee7/cdi/Services.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.RequestScoped import javax.inject.Inject @RequestScoped class MessageService { def message: String = "Hello MessageService!!" def message(prefix: String, suffix: String): String = prefix + message + suffix } @RequestScoped class WrappedService { @Inject var messageService: MessageService = _ def message: String = messageService.message def message(prefix: String, suffix: String): String = messageService.message(prefix, suffix) }
定義したCDI管理Beanを、@InjectするCDI管理Beanも用意しています。
あと、ダメとはわかりつつInterceptorも効かせるコードも用意してみましょう。
Interceptor用のアノテーション。今回は、トレース用のInterceptorを用意してみるものとします。
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 } }
src/main/scala/org/littlewings/javaee7/cdi/TracingServices.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.RequestScoped import javax.inject.Inject @RequestScoped @EnableTracing class TracingMessageService { def message: String = "Hello MessageService!!" def message(prefix: String, suffix: String): String = prefix + message + suffix } @RequestScoped @EnableTracing class TracingWrappedService { @Inject var messageService: TracingMessageService = _ def message: String = messageService.message def message(prefix: String, suffix: String): String = messageService.message(prefix, suffix) }
Apache DeltaSpikeのTest用モジュールのモックサポートの有効化
実は、ここが1番ハマりました。
Apache DeltaSpikeでモックを利用する機能を有効化するためには、「META-INF/apache-deltaspike.properties」というファイルを作成し、「deltaspike.testcontrol.mock-support.allow_mocked_beans」、「deltaspike.testcontrol.mock-support.allow_mocked_producers」をtrueとする必要があります。
src/test/resources/META-INF/apache-deltaspike.properties
deltaspike.testcontrol.mock-support.allow_mocked_beans=true deltaspike.testcontrol.mock-support.allow_mocked_producers=true
ところが、これを定義してdeltaspike-test-control-module-apiとdeltaspike-test-control-module-implを使って動作させると、全然読んでくれている雰囲気がありません…。
このプロパティファイルを読み込むクラスが、deltaspike-core-implに入っているからです。
https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/core/impl/src/main/java/org/apache/deltaspike/core/impl/config/DefaultConfigSourceProvider.java#L40
特にdeltaspike-core-implはなくてもテストは動いてしまうため、最初なんで有効化されないか全然わかりませんでした…。
なお、Apache DeltaSpikeのモックサポートの機能を有効化しないままこの機能を使おうとすると、以下のようなエラーになります。
java.lang.IllegalStateException: The support for mocked CDI-Beans is disabled due to a reduced portability across different CDI-implementations. Please set 'deltaspike.testcontrol.mock-support.allow_mocked_beans' and/or 'deltaspike.testcontrol.mock-support.allow_mocked_producers' to 'true' (in 'META-INF/apache-deltaspike.properties') on your test-classpath. at org.apache.deltaspike.testcontrol.impl.mock.AbstractMockManager.addMock(AbstractMockManager.java:41) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.jboss.weld.bean.proxy.AbstractBeanInstance.invoke(AbstractBeanInstance.java:38) at org.jboss.weld.bean.proxy.ProxyMethodHandler.invoke(ProxyMethodHandler.java:100) at org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager$1727722726$Proxy$_$$_WeldClientProxy.addMock(Unknown Source)
使ってみる
では、ようやくですがApache DeltaSpikeのモックサポートを使ってみましょう。
とりあえず最初は、モックを使わないテストと、モックを使ったテストを並べて書いてみます。
src/test/scala/org/littlewings/javaee7/cdi/SimpleCdiDeltaSpikeMockTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class SimpleCdiDeltaSpikeMockTest extends JUnitSuite with Matchers { @Inject var messageService: MessageService = _ @Test def simpleCase(): Unit = { messageService.message should be("Hello MessageService!!") messageService.message("[", "]") should be("[Hello MessageService!!]") } @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { mockManager.addMock(new MessageService { override def message: String = "This is Mock!!" }) messageService.message should be("This is Mock!!") messageService.message("[", "]") should be("[This is Mock!!]") } }
ふつうに使う方は、単に@InjectしたCDI管理Beanをテストしているだけです。
@Inject var messageService: MessageService = _ @Test def simpleCase(): Unit = { messageService.message should be("Hello MessageService!!") messageService.message("[", "]") should be("[Hello MessageService!!]") }
モックを使う場合は、DynamicMockManagerを@Injectして、DynamicMockManager#addMockでモックとするインスタンス(ここではサブクラスを作っていますが)を渡します。
@Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { mockManager.addMock(new MessageService { override def message: String = "This is Mock!!" }) messageService.message should be("This is Mock!!") messageService.message("[", "]") should be("[This is Mock!!]") }
なお、今回は使いませんが、DynamicMockManager#addMockには第2引数で可変長引数でQualifierを取ることもできます。
内部で別のCDI管理Beanを@Injectしているものについても、対象のクラスをDynamicMockManager#addMockすれば差し替えてくれるようです。
src/test/scala/org/littlewings/javaee7/cdi/CascadingCdiDeltaSpikeMockTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CascadingCdiDeltaSpikeMockTest extends JUnitSuite with Matchers { @Inject var wrappedService: WrappedService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { mockManager.addMock(new MessageService() { override def message: String = "This is Mock!!" }) wrappedService.message should be("This is Mock!!") wrappedService.message("[", "]") should be("[This is Mock!!]") } }
動いてそうですね。
Mockitoを使う
続いて、Mockitoと統合してみます。
Mockitoを使う場合は、Mockito#mockやMockito#spyで作成したモックやSPYをDynamicMockManager#addMockに渡せばOKです。
src/test/scala/org/littlewings/javaee7/cdi/SimpleCdiDeltaSpikeMockWithMockitoTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito._ import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class SimpleCdiDeltaSpikeMockWithMockitoTest extends JUnitSuite with Matchers { @Inject var messageService: MessageService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { val messageServiceMock = mock(classOf[MessageService]) when(messageServiceMock.message).thenReturn("Hello Mock!!") when(messageServiceMock.message("[", "]")).thenReturn("Hello prefix & suffix Mock!!") mockManager.addMock(messageServiceMock) messageService.message should be("Hello Mock!!") messageService.message("[", "]") should be("Hello prefix & suffix Mock!!") } @Test def withSpy(): Unit = { val messageServiceSpy = spy(classOf[MessageService]) when(messageServiceSpy.message).thenReturn("Hello Mock!!") mockManager.addMock(messageServiceSpy) messageService.message should be("Hello Mock!!") messageService.message("[", "]") should be("[Hello Mock!!]") } }
別のCDI管理Beanを@Injectしているものでも、Mockitoを使わない時と同じように差し替えられます。
src/test/scala/org/littlewings/javaee7/cdi/CascadingCdiDeltaSpikeMockWithMockitoTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito._ import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CascadingCdiDeltaSpikeMockWithMockitoTest extends JUnitSuite with Matchers { @Inject var wrappedService: WrappedService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { val messageServiceMock = mock(classOf[MessageService]) when(messageServiceMock.message).thenReturn("Hello Mock!!") when(messageServiceMock.message("[", "]")).thenReturn("[Hello Mock!!]") mockManager.addMock(messageServiceMock) wrappedService.message should be("Hello Mock!!") wrappedService.message("[", "]") should be("[Hello Mock!!]") } @Test def withSpy(): Unit = { val messageServiceSpy = spy(classOf[MessageService]) when(messageServiceSpy.message).thenReturn("Hello Mock!!") mockManager.addMock(messageServiceSpy) wrappedService.message should be("Hello Mock!!") wrappedService.message("[", "]") should be("[Hello Mock!!]") } }
Interceptorを適用してみる
さて、ダメだと書かれているものの、Interceptorが適用されているCDI管理Beanも試してみましょう。
試してみると、確かにモックに差し替わらなくなります。
src/test/scala/org/littlewings/javaee7/cdi/SimpleCdiDeltaSpikeMockWithInterceptorTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class SimpleCdiDeltaSpikeMockWithInterceptorTest extends JUnitSuite with Matchers { @Inject var messageService: TracingMessageService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { mockManager.addMock(new TracingMessageService { override def message: String = "This is Mock!!" }) messageService.message should be("Hello MessageService!!") messageService.message("[", "]") should be("[Hello MessageService!!]") } }
Mockitoを使っても一緒ですし、
src/test/scala/org/littlewings/javaee7/cdi/SimpleCdiDeltaSpikeMockWithMockitoWithInterceptorTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito._ import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class SimpleCdiDeltaSpikeMockWithMockitoWithInterceptorTest extends JUnitSuite with Matchers { @Inject var messageService: TracingMessageService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { val messageServiceMock = mock(classOf[TracingMessageService]) when(messageServiceMock.message).thenReturn("Hello Mock!!") when(messageServiceMock.message("[", "]")).thenReturn("Hello prefix & suffix Mock!!") mockManager.addMock(messageServiceMock) messageService.message should be("Hello MessageService!!") messageService.message("[", "]") should be("[Hello MessageService!!]") } }
別のCDI管理Beanを@Injectしているものについても、やっぱりダメです。
src/test/scala/org/littlewings/javaee7/cdi/CascadingCdiDeltaSpikeMockWithInterceptorTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CascadingCdiDeltaSpikeMockWithInterceptorTest extends JUnitSuite with Matchers { @Inject var wrappedService: TracingWrappedService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { mockManager.addMock(new TracingMessageService() { override def message: String = "This is Mock!!" }) wrappedService.message should be("Hello MessageService!!") wrappedService.message("[", "]") should be("[Hello MessageService!!]") } }
src/test/scala/org/littlewings/javaee7/cdi/CascadingCdiDeltaSpikeMockWithMockitoWithInterceptorTest.scala
package org.littlewings.javaee7.cdi import javax.inject.Inject import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.apache.deltaspike.testcontrol.api.mock.DynamicMockManager import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito._ import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class CascadingCdiDeltaSpikeMockWithMockitoWithInterceptorTest extends JUnitSuite with Matchers { @Inject var wrappedService: TracingWrappedService = _ @Inject var mockManager: DynamicMockManager = _ @Test def withMock(): Unit = { val messageServiceMock = mock(classOf[TracingMessageService]) when(messageServiceMock.message).thenReturn("Hello Mock!!") when(messageServiceMock.message("[", "]")).thenReturn("[Hello Mock!!]") mockManager.addMock(messageServiceMock) wrappedService.message should be("Hello MessageService!!") wrappedService.message("[", "]") should be("[Hello MessageService!!]") } @Test def withSpy(): Unit = { val messageServiceSpy = spy(classOf[TracingMessageService]) when(messageServiceSpy.message).thenReturn("Hello Mock!!") mockManager.addMock(messageServiceSpy) wrappedService.message should be("Hello MessageService!!") wrappedService.message("[", "]") should be("[Hello MessageService!!]") } }
まとめ
Apache DeltaSpikeで、モックを使ったテストを試してみました。InterceptorがかかったCDI管理Beanをモックにできないのはちょっと残念な感じもしますが、一応押さえておきましょう。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-testing-deltaspike-with-mock