CLOVER🍀

That was when it all began.

Scala × Arquillianで、Java EEのテスト(WildFly、Java EE 7版)

Java EE 7やWildFlyが出てから、全然触っていなかったArquillianをそろそろまた使ってみようかということで。

前にもArquillianを使ったエントリを書いていますが、基本的にはその時と同じです。前提は、以下とします。

参考にしたリソースは、いろいろありますが代表的なところはこんなところです。

Arquillianではじめるコンテナを使ったテスト
http://backpaper0.github.io/ghosts/arquillian.html
pom-ee7web.xml
https://github.com/nekop/java-examples/blob/master/maven/pom-ee7web.xml
Arquillian Guides / Getting Started
http://arquillian.org/guides/getting_started/
Arquillian Guides / Creating Deployable Archives with ShrinkWrap
http://arquillian.org/guides/shrinkwrap_introduction/
ShrinkWrap
https://developer.jboss.org/wiki/ShrinkWrap

では、いってみましょう。

なお、順番はRemote、Managedの順で行いました。

sbtの設定

まずはビルドツールである、sbtの設定から。
build.sbt

name := "arquillian-getting-started"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.2"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-unchecked", "-deprecation", "-feature")

incOptions := incOptions.value.withNameHashing(true)

fork in Test := true

// parallelExecution in Test := false

jetty()

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee7-web." + artifact.extension
}

makePomConfiguration := makePomConfiguration.value.copy(file = new File("pom.xml"))

resolvers += "JBoss Public Maven Repository Group" at "https://repository.jboss.org/nexus/content/groups/public/"

// Managedの時は必要
// envVars in Test += ("JBOSS_HOME", "../wildfly-8.1.0.Final")

val jettyVersion = "9.2.2.v20140723"

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion % "container",
  "org.jboss.spec" % "jboss-javaee-web-7.0" % "1.0.1.Final" % "provided",
  // "org.wildfly" % "wildfly-arquillian-container-managed" % "8.1.0.Final" % "test",  // Managed
  "org.wildfly" % "wildfly-arquillian-container-remote" % "8.1.0.Final" % "test",  // Remote
  "org.jboss.arquillian.protocol" % "arquillian-protocol-servlet" % "1.1.5.Final" % "test",  // Remote
  "org.jboss.arquillian.junit" % "arquillian-junit-container" % "1.1.5.Final" % "test",
  "org.jboss.shrinkwrap.resolver" % "shrinkwrap-resolver-depchain" % "2.2.0-alpha-2" % "test",
  "org.scalatest" %% "scalatest" % "2.2.1" % "test",
  "junit" % "junit" % "4.11" % "test",
  "org.jboss.resteasy" % "resteasy-client" % "3.0.8.Final" % "test",  // テストでのHTTPリクエスト実行に使用
  "org.apache.commons" % "commons-lang3" % "3.3.2"  // ShrinkWrap Maven Resolverのサンプル的に使用
)

いきなり、Remote/Managedごちゃ混ぜですが。

ところで、このくらいのシンプルな依存関係の定義で、全然問題なく設定できました。以前はexclude書いたりいろいろしてましたが、sbt側で何か変わったのかなぁ…。

WARファイルとしても作成できるように、xsbt-web-pluginの設定も足しています。
project/plugins.sbt

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.0.0-M6")

で、build.sbtに戻りまして…sbtではbomは使えない(はず)なので、依存関係は普通に定義します。

Remoteを使う場合とManagedを使う場合の差は、以下になります。

// 環境変数の設定
// Managedの時は必要
// envVars in Test += ("JBOSS_HOME", "../wildfly-8.1.0.Final")

// 依存関係の定義
  // "org.wildfly" % "wildfly-arquillian-container-managed" % "8.1.0.Final" % "test",  // Managed
  "org.wildfly" % "wildfly-arquillian-container-remote" % "8.1.0.Final" % "test",  // Remote
  "org.jboss.arquillian.protocol" % "arquillian-protocol-servlet" % "1.1.5.Final" % "test",  // Remote

上記は、Remoteを使う前提で書いているので、Managedの部分はコメントアウトしています。

forkはtrueにしなくてもいいと思いますが、デプロイするアーカイブを同じ名前にしたりするなら、テストの並列実行はオフにした方がよいと思います。

// parallelExecution in Test := false

今回は、並列実行を明示的に無効にしませんでしたが、特に問題なかったです。

ただ、最初はWARファイル名を指定していて、これが被っていたのでデプロイとアンデプロイが同時に行われ、エラーを見ることになりました…。

最後に、ShrinkWrapのMaven Resolverを使うためにpom.xmlを生成するのですが、この時「pom.xml」で作りたかったので、以下の設定を加えました。

makePomConfiguration := makePomConfiguration.value.copy(file = new File("pom.xml"))

これで、「makePom」で生成されるファイルがプロジェクト直下で、「pom.xml」という名前になります。
*この設定を入れなくても、pom.xmlの生成自体は可能です

テスト対象コードを書く

それでは、テスト対象のコードを書いてみましょう。

CDIを使って管理するServiceクラス。
src/main/scala/org/littlewings/javaee7/service/CalcService.scala

package org.littlewings.javaee7.service

import javax.enterprise.context.RequestScoped

@RequestScoped
class CalcService {
  def add(p1: Int, p2: Int): Int =
    p1 + p2

  def multiply(p1: Int, p2: Int): Int =
    p1 * p2
}

JAX-RS関連。先に作ったクラスを使用します。
src/main/scala/org/littlewings/javaee7/rest/HelloResource.scala

package org.littlewings.javaee7.rest

import javax.inject.Inject
import javax.ws.rs.{GET, Path, Produces, QueryParam}
import javax.ws.rs.core.MediaType

import org.apache.commons.lang3.StringUtils

import org.littlewings.javaee7.service.CalcService

@Path("hello")
class HelloResource {
  @Inject
  private var calcService: CalcService = _

  @GET
  @Path("index")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def index: String =
    StringUtils.repeat("Hello World ", 2).trim

  @GET
  @Path("add")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def add(@QueryParam("p1") p1: Int, @QueryParam("p2") p2: Int): String =
    calcService.add(p1, p2).toString
}

Scalaを使っている時点で依存関係が追加されているのですが、ここでさらにムダにCommons Lang3を使っています。こういうのを使っても、ShrinkWrapでうまくデプロイされることを確認したかったので。

一応、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

arquillian.xmlを作る

Remote用に、arquillian.xmlを作成しておきます。
src/test/resources/arquillian.xml

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://jboss.org/schema/arquillian
                                http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

  <defaultProtocol type="Servlet 3.0"/>

</arquillian>

まあ、これはなくてもよさそうな…。

テストコード

それでは、テストコードを書いていきます。

CDIでの管理対象クラスのテストコード

まずは、簡単なCDIの管理対象として定義したクラスのテストコードから。

import文から。
src/test/scala/org/littlewings/javaee7/service/CalcServiceTest.scala

package org.littlewings.javaee7.service

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.jboss.shrinkwrap.resolver.api.maven.Maven

import org.scalatest.Matchers._
import org.scalatest.junit.JUnitSuite

import org.junit.Test
import org.junit.runner.RunWith

object定義。Scalaではstaticメソッドがないため、Arquillianのデプロイ対象のアーカイブは、ここで定義します。

object CalcServiceTest {
  @Deployment
  def createDeployment: WebArchive =
   ShrinkWrap
     .create(classOf[WebArchive])
     .addPackages(true, classOf[CalcService].getPackage.getName)
     .addAsLibraries {
       Maven
         .resolver
         .loadPomFromFile("pom.xml")
         .importRuntimeDependencies
         .resolve("org.scalatest:scalatest_2.11:2.2.1")
         .withTransitivity
         .asFile: _*
     }
}

…ありもしないpom.xmlをロードして依存関係を解決するコードが入っていますが、こちらは後でなんとかします。

あと、ScalaTestですが、こちらはデプロイ対象に含めないとうまく動きませんでした。これは、前に試した時もそうでした。で、文字列でハードコードしているのですが、かといってMaven ResolverのimportRuntimeAndTestDependenciesメソッドとか使ってしまうと、Arquillianまで含めて持っていってしまうので、ここは仕方なく割り切りました。

デプロイするWARファイルの名前は、ShrinkWrapに決めてもらいます。

   ShrinkWrap
     .create(classOf[WebArchive])

テストの定義。

@RunWith(classOf[Arquillian])
class CalcServiceTest extends JUnitSuite {
  @Inject
  private var calcService: CalcService = _

  @Test
  def add1Test(): Unit =
    calcService.add(1, 2) should be (3)

  @Test
  def add2Test(): Unit =
    calcService.add(2, 3) should be (5)

  @Test
  def add3Test(): Unit =
    calcService.add(1, 1) should be (3)

  @Test
  def multiply1Test(): Unit =
    calcService.multiply(1, 3) should be (3)

  @Test
  def multiply2Test(): Unit =
    calcService.multiply(3, 3) should be (9)
}

ScalaTestを使っているとはいえ、Arquillianを使用する時は@RunWithアノテーションを使用したJUnitの形になるため、ここはJUnitSuiteを継承したサブクラスとして作成しています。

ちなみに、失敗するテストが混じっています。

JAX-RSのテストコード

続いて、JAX-RS側のテストコードを作成します。

import文からobject定義まで、一気にいきます。
src/test/scala/org/littlewings/javaee7/rest/HelloResourceTest.scala

package org.littlewings.javaee7.rest

import java.net.URL

import javax.ws.rs.ApplicationPath
import javax.ws.rs.client.ClientBuilder

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.jboss.shrinkwrap.resolver.api.maven.Maven

import org.littlewings.javaee7.service.CalcService

import org.scalatest.Matchers._
import org.scalatest.junit.JUnitSuite

import org.junit.Test
import org.junit.runner.RunWith

object HelloResourceTest {
  @Deployment
  def createDeployment: WebArchive =
   ShrinkWrap
     .create(classOf[WebArchive])
     .addPackages(true, classOf[HelloResource].getPackage.getName)
     .addClass(classOf[CalcService])
     .addAsLibraries {
       Maven
         .resolver
         .loadPomFromFile("pom.xml")
         .importRuntimeDependencies
         .resolve("org.scalatest:scalatest_2.11:2.2.1")
         .withTransitivity
         .asFile: _*
     }
}

先ほどのCDIでの管理対象の時と同じと思いきや、1行増えています。

     .addClass(classOf[CalcService])

これは、テスト対象のJAX-RSのリソースクラスが、このクラスに依存しているからですね…。一緒にデプロイしてあげないと、動きませんよ、と。

続いて、テストコード本体。

@RunWith(classOf[Arquillian])
@RunAsClient
class HelloResourceTest extends JUnitSuite {
  private val resourcePrefix: String =
    classOf[JaxrsApplication]
      .getAnnotation(classOf[ApplicationPath])
      .value

  @ArquillianResource
  private var deploymentUrl: URL = _

  @Test
  def helloWorld1Test(): Unit = {
    val client = ClientBuilder.newBuilder.build

    try {
      val response =
        client
          .target(s"${deploymentUrl}${resourcePrefix}/hello/index")
          .request
          .get

      response.readEntity(classOf[String]) should be ("Hello World Hello World")

      response.close()
    } finally {
      client.close()
    }
  }

  @Test
  def helloWorld2Test(): Unit = {
    val client = ClientBuilder.newBuilder.build

    try {
      val response =
        client
          .target(s"${deploymentUrl}${resourcePrefix}/hello/index")
          .request
          .get

      response.readEntity(classOf[String]) should be ("Hello World")

      response.close()
    } finally {
      client.close()
    }
  }

  @Test
  def addTest(): Unit = {
    val client = ClientBuilder.newBuilder.build

    try {
      val response =
        client
          .target(s"${deploymentUrl}${resourcePrefix}/hello/add?p1=1&p2=3")
          .request
          .get

      response.readEntity(classOf[String]) should be ("4")

      response.close()
    } finally {
      client.close()
    }
  }
}

JAX-RSクライアントでリクエストを投げて、その結果を確認しているコードですが、少し先ほどとは違うところがあります。

クラスの定義に、@RunAsClientアノテーションを追加して、クライアント側で動作することを示しています。

@RunWith(classOf[Arquillian])
@RunAsClient
class HelloResourceTest extends JUnitSuite {

アクセス先のURLの基底部分は、JAX-RSのクラスから抜き出したり@ArquillianResourceアノテーションで取得します。

  private val resourcePrefix: String =
    classOf[JaxrsApplication]
      .getAnnotation(classOf[ApplicationPath])
      .value

  @ArquillianResource
  private var deploymentUrl: URL = _

これで

      val response =
        client
          .target(s"${deploymentUrl}${resourcePrefix}/hello/index")
          .request
          .get

のような形でURLを組み立てることができます。

実行(Remote)

それでは、テストを実行してみましょう。

…の前に、pom.xmlを作成します。

> makePom
[info] Wrote /xxxxx/arquillian-getting-started/pom.xml
[success] Total time: 0 s, completed 2014/08/30 23:28:44

続いて、WildFlyを起動しておきます。

$ wildfly-8.1.0.Final/bin/standalone.sh

では、テスト実行。

> test

XNIO、JBoss Remotingの表示がちょこっと出て、テストが実行されます。

[info] HelloResourceTest:
8 30, 2014 11:29:43 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.2.2.Final
8 30, 2014 11:29:43 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.2.2.Final
8 30, 2014 11:29:43 午後 org.jboss.remoting3.EndpointImpl <clinit>
INFO: JBoss Remoting version 4.0.3.Final
[info] - helloWorld1Test
[info] - helloWorld2Test *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException: "Hello World[ Hello World]" was not equal to "Hello World[]" (HelloResourceTest.scala:80)
[info] - addTest
[info] CalcServiceTest:
[info] - add1Test
[info] - add2Test
[info] - add3Test *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException: 2 was not equal to 3 (CalcServiceTest.scala:49)
[info] - multiply1Test
[info] - multiply2Test
[info] Run completed in 35 seconds, 576 milliseconds.
[info] Total number of tests run: 8
[info] Suites: completed 2, aborted 0
[info] Tests: succeeded 6, failed 2, canceled 0, ignored 0, pending 0
[info] *** 2 TESTS FAILED ***
[error] Failed tests:
[error] 	org.littlewings.javaee7.service.CalcServiceTest
[error] 	org.littlewings.javaee7.rest.HelloResourceTest
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 36 s, completed 2014/08/30 23:30:13

失敗するテストも入れていたので結果はNGですが、ちゃんとScalaTestの結果として表示されました。

OKそうですね!

実行(Managed)

WildFlyは、停止しておきます。

あとは、先ほどのbuild.sbtで環境変数JBOSS_HOMEを追加して

// Managedの時は必要
envVars in Test += ("JBOSS_HOME", "../wildfly-8.1.0.Final")

依存関係を、Managedの方を使うように設定します。

  "org.wildfly" % "wildfly-arquillian-container-managed" % "8.1.0.Final" % "test",  // Managed

以下の2つは不要です。

  // "org.wildfly" % "wildfly-arquillian-container-remote" % "8.1.0.Final" % "test",  // Remote
  // "org.jboss.arquillian.protocol" % "arquillian-protocol-servlet" % "1.1.5.Final" % "test",  // Remote

あとは、

> test

でWildFlyが起動されテストが行われます。結果は端折ります〜。

一応、基本的なところはできたのではないでしょうか…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/arquillian-getting-started

その他、確認したりしたソースなどはこちらです。
ResolveStage
https://github.com/shrinkwrap/resolver/blob/master/api/src/main/java/org/jboss/shrinkwrap/resolver/api/ResolveStage.java
PomEquippedResolveStageBase
https://github.com/shrinkwrap/resolver/blob/master/api-maven/src/main/java/org/jboss/shrinkwrap/resolver/api/maven/PomEquippedResolveStageBase.java
ScopeType
https://github.com/shrinkwrap/resolver/blob/master/api-maven/src/main/java/org/jboss/shrinkwrap/resolver/api/maven/ScopeType.java

ShrinkWrap Javadoc
http://docs.jboss.org/shrinkwrap/