CLOVER🍀

That was when it all began.

Apache DeltaSpikeでCDI管理Beanをモックを使ってテストする

Apache DeltaSpikeのテスト用モジュールには、モックを使ったテストができる機能があります。

Mock Frameworks

インターセプターがかかった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