CLOVER🍀

That was when it all began.

Scala × Arquillianで、Java EEのテスト

JBoss ASにデプロイしながら進めていくのもいいのですが、ここはひとつ、テストコードから実行してみようと思いまして。今まで名前だけは聞いたことのあった、Arquillianを使ってみます。

Arquillian
http://arquillian.org/

日本語でもいくつか情報が出ているのと、Getting Startedなどを参考に。

とさらっと書くと聞こえはいいのですが、相変わらずScala+sbtでチャレンジしたので、死ぬほどハマりました。

まあ、グチるのは後にして進めてみましょう。今回の目標は、EJB+JPAでテストができること、とします。

テストケースは、ScalaTestとかで書いてみようかとも思ったのですが、なんかハマりそうだったのでいったんJUnitにしました。…結果、まずはJUnitを選んでよかったと思います。

テスト対象

とりあえず、EntityとEJBを用意します。

src/main/scala/javaee6/web/entity/Entity.scala

package javaee6.web.entity

import scala.beans.BeanProperty

import javax.persistence.{Column, Entity, Id, Persistence, Table, Version}

object User {
  def apply(id: Int, firstName: String, lastName: String, age: Int): User = {
    val user = new User
    user.id = id
    user.firstName = firstName
    user.lastName = lastName
    user.age = age
    user
  }
}

@SerialVersionUID(1L)
@Entity
@Table(name = "user")
class User extends Serializable {
  @Id
  @BeanProperty
  var id: Int = _

  @Column(name = "first_name")
  @BeanProperty
  var firstName: String = _

  @Column(name = "last_name")
  @BeanProperty var lastName: String = _

  @Column
  @BeanProperty
  var age: Int = _

  @Column(name = "version_no")
  @Version
  @BeanProperty
  var versionNo: Int = _

  override def toString(): String =
    s"id = $id, firstName = $firstName, lastName = $lastName, age = $age, versionNo = $versionNo"
}

src/main/scala/javaee6/web/service/UserService.scala

package javaee6.web.service

import scala.collection.JavaConverters._

import javax.ejb.{Local, LocalBean, Stateless}
import javax.persistence.{EntityManager, PersistenceContext}

import javaee6.web.entity.User

@Stateless
@LocalBean
class UserService {
  @PersistenceContext
  var entityManager: EntityManager = _

  def find(id: Int): User =
    entityManager
      .find(classOf[User], id)

  def findAll: Iterable[User] =
    entityManager
      .createQuery("SELECT u FROM User u")
      .getResultList
      .asScala
      .asInstanceOf[Iterable[User]]
}

永続性ユニットがひとつしかなかったら、@PersistenceContextのunitNameって設定しなくていいんですね。知らなかったです。

永続性ユニットの定義。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
  <persistence-unit name="javaee6.web.pu" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:jboss/datasources/mysqlXaDs</jta-data-source>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
      <property name="hibernate.show_sql" value="true" />
      <property name="hibernate.format_sql" value="true" />
    </properties>
  </persistence-unit>
</persistence>

使用するXAデータソースは、以後の環境変数$JBOSS_HOMEで指定するJBoss AS上で作成済みとします。

で、以下のファイルは作成するだけ。

src/main/webapp/WEB-INF/beans.xml

Arquillianを実行する用意をする

まずは、こちらやその他日本語情報を参考に環境設定。

Getting Started
http://arquillian.org/guides/getting_started_ja/

Arquillianは、「Remote」、「Managed」、「Embedded」の3種類のコンテナをサポートしているらしいのですが、今回は「Managed」を使用します。「Managed」の場合は、Arquillianがインストール済みのアプリケーションサーバを起動するので、インストール先を設定する必要があります。

JBoss ASの場合は「JBOSS_HOME」を設定すればよいみたい?

で、今回はEJBのテストを使用と思いますので、こんなデータに対して

mysql> SELECT * FROM user;
+----+------------+-----------+------+------------+
| id | first_name | last_name | age  | version_no |
+----+------------+-----------+------+------------+
|  1 | カツオ     | 磯野      |   11 |          0 |
|  2 | ワカメ     | 磯野      |    9 |          0 |
|  3 | タラオ     | フグ田    |    3 |          0 |
+----+------------+-----------+------+------------+
3 rows in set (0.06 sec)

こんなテストコードを作成しました。
src/test/scala/javaee6/web/service/UserServiceTest.scala

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

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

import org.junit.Assert._
import org.hamcrest.CoreMatchers._

import javaee6.web.entity.User

@RunWith(classOf[Arquillian])
class UserServiceTest {
  @Inject
  var userService: UserService = _

  @Test
  def findUserTest: Unit = {
    val katsuo = userService.find(1)

    assertThat(katsuo.firstName, is("カツオ"))
    assertThat(katsuo.lastName, is("磯野"))
    assertThat(katsuo.age, is(11))
  }
}

object UserServiceTest {
  @Deployment
  def createDeployment: WebArchive =
    ShrinkWrap
      .create(classOf[WebArchive], "javaee6-web.war")
      //.addClasses(classOf[UserService], classOf[User]) // Class単位で指定
      .addPackages(true, "javaee6.web")
      .addAsResource("META-INF/persistence.xml")
      .addAsWebInfResource(new File("src/main/webapp/WEB-INF/beans.xml"))
      .addAsLibraries {
        Maven
          .resolver
          .resolve("org.scala-lang:scala-library:2.10.3")
          .withTransitivity
          .asFile: _*
      }
      //.addAsLibrary(toJarPathOfClass(classOf[ScalaObject]))  // ローカルファイルシステムから指定

  private def toJarPathOfClass(clazz: Class[_]): File = {
    val resource = clazz.getName.split('.').mkString("/", "/", ".class")
    val path = getClass.getResource(resource).getPath
    val indexOfFileScheme = path.indexOf("file:") + 5
    val indexOfSeparator = path.lastIndexOf('!')
    new File(path.substring(indexOfFileScheme, indexOfSeparator))
  }
}

ここでのポイントは、テスト対象クラスに@RunWithアノテーションを指定していること、

@RunWith(classOf[Arquillian])
class UserServiceTest {

コンパニオンオブジェクトに@Deploymentアノテーションを指定したメソッドを定義していることです。

  def createDeployment: WebArchive =
    ShrinkWrap
      .create(classOf[WebArchive], "javaee6-web.war")
      //.addClasses(classOf[UserService], classOf[User]) // Class単位で指定
      .addPackages(true, "javaee6.web")
      .addAsResource("META-INF/persistence.xml")
      .addAsWebInfResource(new File("src/main/webapp/WEB-INF/beans.xml"))
      .addAsLibraries {
        Maven
          .resolver
          .resolve("org.scala-lang:scala-library:2.10.3")
          .withTransitivity
          .asFile: _*
      }
      //.addAsLibrary(toJarPathOfClass(classOf[ScalaObject]))  // ローカルファイルシステムから指定

このメソッドで、アプリケーションサーバにデプロイする対象を決めていきます。

また、@Deploymentアノテーションを付与したメソッドはstaticメソッドである必要があるのですが、コンパニオンオブジェクトでとりあえずどうにかなりました。

最後になりましたが、sbtの定義はこちらです。
build.sbt

name := "arquillian-getting-started"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "littlewings"

resolvers += "Public JBoss Group" at "http://repository.jboss.org/nexus/content/groups/public-jboss"

fork in Test := true

envVars in Test += ("JBOSS_HOME", "/path/to/jboss-as-7.1.1.Final")

libraryDependencies ++= Seq(
  "org.jboss.spec" % "jboss-javaee-web-6.0" % "3.0.2.Final" % "provided",
  "org.jboss.as" % "jboss-as-arquillian-container-managed" % "7.1.1.Final" % "test" excludeAll(
    ExclusionRule(organization = "org.jboss.shrinkwrap"),
    ExclusionRule(organization = "org.jboss.shrinkwrap.descriptors")
  ),
  "org.jboss.arquillian.junit" % "arquillian-junit-container" % "1.1.1.Final" % "test" excludeAll(
    ExclusionRule(organization = "org.jboss.shrinkwrap"),
    ExclusionRule(organization = "org.jboss.shrinkwrap.descriptors")
  ),
  "org.jboss.shrinkwrap" % "shrinkwrap-api" % "1.0.0-cr-1" % "test",
  "org.jboss.shrinkwrap" % "shrinkwrap-impl-base" % "1.1.2" % "test",
  "org.jboss.shrinkwrap.descriptors" % "shrinkwrap-descriptors-spi" % "2.0.0-alpha-3" % "test",
  "org.jboss.shrinkwrap.resolver" % "shrinkwrap-resolver-depchain" % "2.0.1" % "test" excludeAll(
    ExclusionRule(organization = "org.jboss.shrinkwrap")
  ),
  "junit" % "junit" % "4.11" % "test",
  "com.novocode" % "junit-interface" % "0.10" % "test"
)

環境変数、$JBOSS_HOMEにJBoss ASのインストール先を指定しています。

envVars in Test += ("JBOSS_HOME", "/path/to/jboss-as-7.1.1.Final")

あとは、sbtコンソールで

> test

と実行すると、ArquillianがJBoss ASを勝手に起動してデプロイし、テストを実行してくれます。

と、準備と実行までの流れはこんな感じです。

ハマったこと

それはそれは、たくさんハマりました。せっかくなので、トラブルシュートの履歴も書いておきます。

ビルドが通らない

最初にハマったのが、これ。Arquillianはデプロイする対象を、ShrinkWrapのAPIを使って作るのですが、この依存関係が解決できません。

普通に依存関係を定義すると

  "org.jboss.as" % "jboss-as-arquillian-container-managed" % "7.1.1.Final" % "test",
  "org.jboss.arquillian.junit" % "arquillian-junit-container" % "1.1.1.Final" % "test",

といきたいところですが、最終的には

  "org.jboss.as" % "jboss-as-arquillian-container-managed" % "7.1.1.Final" % "test" excludeAll(
    ExclusionRule(organization = "org.jboss.shrinkwrap"),
    ExclusionRule(organization = "org.jboss.shrinkwrap.descriptors")
  ),
  "org.jboss.arquillian.junit" % "arquillian-junit-container" % "1.1.1.Final" % "test" excludeAll(
    ExclusionRule(organization = "org.jboss.shrinkwrap"),
    ExclusionRule(organization = "org.jboss.shrinkwrap.descriptors")
  ),
  "org.jboss.shrinkwrap" % "shrinkwrap-api" % "1.0.0-cr-1" % "test",
  "org.jboss.shrinkwrap" % "shrinkwrap-impl-base" % "1.1.2" % "test",
  "org.jboss.shrinkwrap.descriptors" % "shrinkwrap-descriptors-spi" % "2.0.0-alpha-3" % "test",
  "org.jboss.shrinkwrap.resolver" % "shrinkwrap-resolver-depchain" % "2.0.1" % "test" excludeAll(
    ExclusionRule(organization = "org.jboss.shrinkwrap")
  ),

という形となりました。

なんで、こんなことになったか?この4つの依存関係が、最初の定義だと解決できなかったのです。

[warn] 	::::::::::::::::::::::::::::::::::::::::::::::
[warn] 	::          UNRESOLVED DEPENDENCIES         ::
[warn] 	::::::::::::::::::::::::::::::::::::::::::::::
[warn] 	:: org.jboss.shrinkwrap.descriptors#shrinkwrap-descriptors-spi;${version.shrinkwrap_descriptors}: not found
[warn] 	:: org.jboss.shrinkwrap.descriptors#shrinkwrap-descriptors-api;${version.shrinkwrap_descriptors}: not found
[warn] 	:: org.jboss.shrinkwrap#shrinkwrap-api;1.1.1.Final: not found
[warn] 	:: org.jboss.shrinkwrap#shrinkwrap-impl-base;1.1.1.Final: not found
[warn] 	::::::::::::::::::::::::::::::::::::::::::::::
[trace] Stack trace suppressed: run last *:update for the full output.
[error] (*:update) sbt.ResolveException: unresolved dependency: org.jboss.shrinkwrap.descriptors#shrinkwrap-descriptors-spi;${version.shrinkwrap_descriptors}: not found
[error] unresolved dependency: org.jboss.shrinkwrap.descriptors#shrinkwrap-descriptors-api;${version.shrinkwrap_descriptors}: not found
[error] unresolved dependency: org.jboss.shrinkwrap#shrinkwrap-api;1.1.1.Final: not found
[error] unresolved dependency: org.jboss.shrinkwrap#shrinkwrap-impl-base;1.1.1.Final: not found

これと同じものを踏んでいるみたいです。

Wrong dependency version resolution leading to build halt
https://github.com/sbt/sbt/issues/647

要は、間違った依存関係を引っ張って来ちゃうと。仕方がないので、ArquillianからShrinkWrapへの依存関係を解決するために、自分で依存関係を定義しましたとさ。

参照したpom。
https://repository.jboss.org/nexus/content/groups/public/org/jboss/arquillian/arquillian-parent/1.1.1.Final/arquillian-parent-1.1.1.Final.pom
https://repository.jboss.org/nexus/content/groups/public/org/jboss/arquillian/arquillian-build/1.1.1.Final/arquillian-build-1.1.1.Final.pom

sbtがJUnitテストケースを認識しない

これは、単純に知らなかっただけ。junit-interfaceを足してあげる必要があるようです。

Testing
http://www.scala-sbt.org/0.12.3/docs/Detailed-Topics/Testing#junit

junit-interface
https://github.com/szeiger/junit-interface

sbtで環境変数を定義する方法でハマる

Arquillianを使う時には、環境変数$JBOSS_HOMEを用意するか、arquillian.xmlを用意する必要があるみたいなのですが、後者は置いておいて…sbtで環境変数を定義する方法が最初分からず、javaOptionsとかに足してもまったく効果がなかったのですが、sbt 0.13から追加されたenvVarsを使用することで、なんとかなりました。

これですね。

envVars in Test += ("JBOSS_HOME", "/path/to/jboss-as-7.1.1.Final")
デプロイ時に、Scalaのライブラリが必要

冷静に考えれば、ShrinkWrapでデプロイ対象を作成している以上、依存ライブラリを追加する必要があるんですよね、というところ。ただ、見かけるサンプルはそういうのほとんど書いていないので(そもそもScalaで書く人もいない)、かなりてこずりました。

参考にしたのは、こちら。
Using the ShrinkWrap Maven Resolver for Arquillian Tests
http://java.dzone.com/articles/using-shrinkwrap-maven
ShrinkWrap Resolver 2.0.0 modifications
https://community.jboss.org/thread/175528

sbt使っているから、MavenResolverは使えないのかと思ったのですが、そんなことなかったですね。

ShrinkWrap Resolvers
https://github.com/shrinkwrap/resolver

ここで指定されたアーティファクトを解決するのは、きっと裏側なんでしょう…。

あとは、ドキュメントを参照して実装。

ShrinkWrap
https://community.jboss.org/wiki/ShrinkWrap

Creating Deployable Archives with ShrinkWrap
http://arquillian.org/guides/shrinkwrap_introduction/

Javadoc
http://docs.jboss.org/shrinkwrap/1.0.0-cr-3/

結果、最初はファイルシステムから直接見たりしていたのですが、スッキリ書けるようになりました。コメントアウトしている箇所は、試行錯誤の名残りです。

  @Deployment
  def createDeployment: WebArchive =
    ShrinkWrap
      .create(classOf[WebArchive], "javaee6-web.war")
      //.addClasses(classOf[UserService], classOf[User]) // Class単位で指定
      .addPackages(true, "javaee6.web")
      .addAsResource("META-INF/persistence.xml")
      .addAsWebInfResource(new File("src/main/webapp/WEB-INF/beans.xml"))
      .addAsLibraries {
        Maven
          .resolver
          .resolve("org.scala-lang:scala-library:2.10.3")
          .withTransitivity
          .asFile: _*
      }
      //.addAsLibrary(toJarPathOfClass(classOf[ScalaObject]))  // ローカルファイルシステムから指定

  private def toJarPathOfClass(clazz: Class[_]): File = {
    val resource = clazz.getName.split('.').mkString("/", "/", ".class")
    val path = getClass.getResource(resource).getPath
    val indexOfFileScheme = path.indexOf("file:") + 5
    val indexOfSeparator = path.lastIndexOf('!')
    new File(path.substring(indexOfFileScheme, indexOfSeparator))
  }