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