Java EEで遊ぶ時、WildFlyにデプロイ、もしくはArquillianを一緒に使うか、自分でEmbedded Tomcatのインスタンスを作ってそこで動かすことが多いのですが、そういえばArquillianにもTomcatのサポートがあったなと思い、こちらも使ってみることにしました。
ArquillianのEmbedded Tomcat 7向けのドキュメントですが、こちらを参考にします。
Tomcat 7.0 - Embedded
https://docs.jboss.org/author/display/ARQ/Tomcat+7.0+-+Embedded
なお、Tomcat 7まではArquillianのサポートはEmbeddd、Managed、Remoteがあったみたいなのですが、Tomcat 8ではEmbeddedのみな感じがします。別に困りませんが。
今回は、JAX-RSとCDI管理Beanをテストのサンプルとして作って試します。
準備
環境は、Scala+sbt、そしてScalaTestで行います。JAX-RSの実装はRESTEasy、CDIの実装はWeldです。というか、AqruillianのドキュメントにはCDI使いたかったらWeld Servlet含めなさいって書いてますし…。
build.sbt
name := "arquillian-tomcat-embedded-8" version := "1.0" scalaVersion := "2.11.6" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) fork in Test := true libraryDependencies ++= Seq( "org.jboss.resteasy" % "resteasy-servlet-initializer" % "3.0.11.Final", "org.jboss.resteasy" % "resteasy-cdi" % "3.0.11.Final", "org.jboss.weld.servlet" % "weld-servlet" % "2.2.11.Final", "org.jboss.arquillian.container" % "arquillian-tomcat-embedded-8" % "1.0.0.CR7" % "test", "org.jboss.arquillian.junit" % "arquillian-junit-container" % "1.1.8.Final" % "test", "org.apache.tomcat.embed" % "tomcat-embed-core" % "8.0.21" % "provided", "org.apache.tomcat.embed" % "tomcat-embed-jasper" % "8.0.21" % "provided", "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % "8.0.21" % "provided", "org.scalatest" %% "scalatest" % "2.2.4" % "test", "junit" % "junit" % "4.12" % "test" )
Web用のプロジェクトにすらしませんでした(笑)。なお、今回の構成ではShrinkwrapとEclipse JDTは不要でした。
JSPを使う場合は、Eclipse JDTが必要だったりするのでしょうか。
それから、空のファイルでいいので、beans.xmlが必要です。
src/main/resources/META-INF/beans.xml
src/main/webapp/WEB-INF配下ではないのに違和感がありますが、MavenでWeb用のプロジェクトにしたら、WEB-INF配下でもよくなるのでしょうかね。今回は、ここに置かないとダメでした。
今回、JAX-RSの実装にRESTEasyを使っていますが、CDIとの連携にはweb.xmlでの設定が必要です。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app 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/web-app_3_1.xsd" version="3.1"> <context-param> <param-name>resteasy.injector.factory</param-name> <param-value>org.jboss.resteasy.cdi.CdiInjectorFactory</param-value> </context-param> </web-app>
これは、ArquillianへのDeploymentに含める時に使う感じになります。
Arquillianの設定ファイル。こちらは、ArquillianのTomcat 7向けの設定そのままです。
src/test/resources/arquillian.xml
<?xml version="1.0" encoding="UTF-8"?> <arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://jboss.org/schema/arquillian" xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> <container qualifier="tomcat" default="true"> <configuration> <property name="unpackArchive">true</property> </configuration> </container> </arquillian>
CDIを使う時は、unpackArchiveをtrueにしてね、と書いているのですが、これで後でハマった気が…。
テスト対象のクラス
テスト対象のクラスは、簡単に用意します。
CDI管理Bean。
src/main/scala/org/littlewings/javaee7/service/CalcService.scala
package org.littlewings.javaee7.service import javax.enterprise.context.RequestScoped @RequestScoped class CalcService { def add(a: Int, b: Int) = a + b }
JAX-RS有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala
package org.littlewings.javaee7.rest import javax.ws.rs.ApplicationPath import javax.ws.rs.core.Application @ApplicationPath("rest") class JaxrsApplication extends Application
JAX-RSリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/CalcResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.inject.Inject import javax.ws.rs._ import javax.ws.rs.core.MediaType import org.littlewings.javaee7.service.CalcService @Path("calc") @RequestScoped class CalcResource { @Inject private var calcService: CalcService = _ @GET @Path("add") @Produces(Array(MediaType.TEXT_PLAIN)) def add(@QueryParam("a") @DefaultValue("0") a: Int, @QueryParam("b") @DefaultValue("0") b: Int): Int = calcService.add(a, b) }
単純なサンプルです。
テストコード
あとは、テストコードを書いていきます。まずはCDI管理Bean向けのテストクラス。
src/test/scala/org/littlewings/javaee7/service/CalcServiceTest.scala
package org.littlewings.javaee7.service import java.io.File import javax.inject.Inject import org.jboss.arquillian.container.test.api.Deployment import org.jboss.arquillian.junit.Arquillian import org.jboss.shrinkwrap.api.ShrinkWrap import org.jboss.shrinkwrap.api.spec.WebArchive import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers._ import org.scalatest.junit.JUnitSuite object CalcServiceTest { @Deployment def createDeployment: WebArchive = ShrinkWrap .create(classOf[WebArchive]) } @RunWith(classOf[Arquillian]) class CalcServiceTest extends JUnitSuite { @Inject private var calcService: CalcService = _ @Test def testAdd(): Unit = calcService.add(1, 2) should be(3) }
すごく単純なんですが、実はここでけっこうハマりました…。
デプロイするアーカイブを作成する部分で、普段Arquillianを使うノリでテスト対象のクラスをアーカイブに含めていると、CDIの依存関係の解決に失敗します。同じクラスが2つ存在するようになって、どちらを選べばいいかわからなくなるみたいです。
@Deployment
def createDeployment: WebArchive =
ShrinkWrap
.create(classOf[WebArchive])
.addClass(classOf[CalcService])
こう書いていると
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type CalcService with qualifiers @Default at injection point [BackedAnnotatedField] @Inject private org.littlewings.javaee7.rest.CalcResource.calcService at org.littlewings.javaee7.rest.CalcResource.calcService(CalcResource.java:0)
こんな感じで、デプロイ自体に失敗します。
このあたり、arquillian.xmlでunpackしているところが効いている気がします。
というわけで、こんな感じにシンプルにすると解決。
@Deployment
def createDeployment: WebArchive =
ShrinkWrap
.create(classOf[WebArchive])
JAX-RSリソースクラスのテストコード。
src/test/scala/org/littlewings/javaee7/rest/CalcResourceTest.scala
package org.littlewings.javaee7.rest import java.io.File import java.net.URL import javax.ws.rs.ApplicationPath import org.jboss.arquillian.container.test.api.{Deployment, RunAsClient} import org.jboss.arquillian.junit.Arquillian import org.jboss.arquillian.test.api.ArquillianResource import org.jboss.shrinkwrap.api.ShrinkWrap import org.jboss.shrinkwrap.api.spec.WebArchive import org.junit.Test import org.junit.runner.RunWith import org.scalatest.Matchers._ import org.scalatest.junit.JUnitSuite import scala.io.Source object CalcResourceTest { @Deployment def createDeployment: WebArchive = ShrinkWrap .create(classOf[WebArchive]) .addAsWebInfResource(new File("src/main/webapp/WEB-INF/web.xml")) } @RunWith(classOf[Arquillian]) @RunAsClient class CalcResourceTest extends JUnitSuite { private val resourcePrefix: String = classOf[JaxrsApplication] .getAnnotation(classOf[ApplicationPath]) .value @ArquillianResource private var url: URL = _ @Test def testAdd(): Unit = Source .fromURL(s"${url}${resourcePrefix}/calc/add?a=1&b=2") .mkString should be("3") }
こちらも@RunAsClientを付けた普通のコードとそう変わらないと思いますが、こちらはアーカイブにweb.xmlを含めるようにしています。
@Deployment def createDeployment: WebArchive = ShrinkWrap .create(classOf[WebArchive]) .addAsWebInfResource(new File("src/main/webapp/WEB-INF/web.xml"))
終わりに
これで、Embedded Tomcat 8でも、JAX-RS+CDIのテストができるようになりました。
ただ、これを動かしていると、(Linuxだと)「/tmp」配下にテストを動かすごとに「tomcat-embedded-xxxxxxxxxxxx.tmp」みたいなディレクトリができていくのがちょっと気になります…。これ、自分でEmbedded Tomcatのインスタンスを制御していた時には最後に消すようにしていたのですが…。
まあ、いいかぁ。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/arquillian-tomcat-embedded-8