CLOVER🍀

That was when it all began.

Embedded Tomcat 8でArquillianを使う

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

JAX-RSとCDI連携のためですね。

終わりに

これで、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