CLOVER🍀

That was when it all began.

Apache DeltaSpikeのData Moduleを試す(Test-Control/Arquillianテスト付き)

Apache DeltaSpikeには、Data Moduleというものがあります。

Data Module

サンプルを見ていると雰囲気はなんとなくわかるのですが、JPAでRepositoryパターンを実装するためのもので、

といった、どこかで見たような機能が使えるようになります(Spring Data JPA)。

で、せっかくなので今回のエントリはこういう構成でいってみたいと思います。

  • Data Moduleの簡単な紹介
  • Test-Controlを使った単体テスト
  • Aqruillian(WildFly Remote)を使ってJTAを絡めたインテグレーションテスト

以降、順に書いていきます。

Data Moduleを使ってみる

それでは、まずはData Moduleを使ってみましょう。

準備

Maven依存関係としては、以下のように定義します。

        <!-- Scala -->
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <!-- Java EE Web Profile -->
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- Apache DeltaSpike Core & Data Module -->
        <dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-api</artifactId>
            <version>1.7.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-impl</artifactId>
            <version>1.7.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.modules</groupId>
            <artifactId>deltaspike-data-module-api</artifactId>
            <version>1.7.1</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.modules</groupId>
            <artifactId>deltaspike-data-module-impl</artifactId>
            <version>1.7.1</version>
            <scope>runtime</scope>
        </dependency>

Scalaを使うのは、ご愛嬌…。

Scalaを使うので、Scalaプラグインも足しておきます。
※本質的ではありませんが

            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <scalaVersion>${scala.version}</scalaVersion>
                    <args>
                        <arg>-Xlint</arg>
                        <arg>-unchecked</arg>
                        <arg>-deprecation</arg>
                        <arg>-feature</arg>
                    </args>
                    <recompileMode>incremental</recompileMode>
                </configuration>
            </plugin>

beans.xmlも用意します。
src/main/resources/META-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans 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/beans_1_1.xsd"
       bean-discovery-mode="annotated">
</beans>
Entityを作成する

JPAで使うEntityを作成します。お題は、書籍とします。
src/main/scala/org/littlewings/javaee7/entity/Book.scala

package org.littlewings.javaee7.entity

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

import scala.beans.BeanProperty

object Book {
  def apply(isbn: String, title: String, price: Int): Book = {
    val book = new Book
    book.isbn = isbn
    book.title = title
    book.price = price
    book
  }
}

@Entity
@Table(name = "book")
@SerialVersionUID(1L)
class Book extends Serializable {
  @Id
  @BeanProperty
  var isbn: String = _

  @Column
  @BeanProperty
  var title: String = _

  @Column
  @BeanProperty
  var price: Int = _
}
Data Moduleを使う

Data Moduleの使い方の基本は、@Repositoryアノテーションの付与とEntityRepositoryインターフェースを実装したインターフェースの作成です。
Scalaなのでトレイトになっていますが
src/main/scala/org/littlewings/javaee7/repository/BookRepository.scala

package org.littlewings.javaee7.repository

import org.apache.deltaspike.data.api.{EntityRepository, Query, Repository}
import org.littlewings.javaee7.entity.Book

@Repository
trait BookRepository extends EntityRepository[Book, String]

これだけで、基本的な操作(save/remove/findBy(PrimaryKey)/findAll/count)などはできるようになります。

Repositories

実装自体は、Partial-Beanという仕組みで生成しているようです。

Partial-Bean Module

メソッドの命名規則によるクエリの生成も可能です。

  def findByPriceGreaterThanEqualsOrderByPriceDesc(price: Int): java.util.List[Book]

メソッドの実装自体は、行う必要はありません。

使える命名規則としては、以下のようになっています。

  • Equal
  • NotEqual
  • Like
  • GreaterThan
  • GreaterThanEquals
  • LessThan
  • LessThanEquals
  • Between
  • IsNull
  • IsNotNull

Using Method Expressions

ORDER BYやLIMITといったことも可能です。

Query Ordering

Query Limits

それでも表現できない場合は、@Queryアノテーションを使用してクエリを書きます。
Query Annotations

例えば、こんな感じで。

  @Query("SELECT COUNT(b) FROM Book b WHERE b.price >= ?1")
  def countPriceGreaterThan(price: Int): Long

  @Query("SELECT b FROM Book b WHERE b.title LIKE ?1 ORDER BY b.price DESC")
  def findByTitleLike(title: String): java.util.List[Book]

パラメータは、「?N」なインデックス形式で指定するんですねぇ。こちらもメソッドの実装自体は、行う必要はありません。

QueryResultを使用することで、オプションやページングの指定ができたり

Query Options

Pagination

OptionalやStreamも使える模様。

Java 8 Semantics

細かくは書ききれないので、興味のある方はドキュメントを…。

というわけで、今回はこんなRepositoryを作成しました。
src/main/scala/org/littlewings/javaee7/repository/BookRepository.scala

package org.littlewings.javaee7.repository

import org.apache.deltaspike.data.api.{EntityRepository, Query, Repository}
import org.littlewings.javaee7.entity.Book

@Repository
trait BookRepository extends EntityRepository[Book, String] {
  def findByPriceGreaterThanEqualsOrderByPriceDesc(price: Int): java.util.List[Book]

  @Query("SELECT COUNT(b) FROM Book b WHERE b.price >= ?1")
  def countPriceGreaterThan(price: Int): Long

  @Query("SELECT b FROM Book b WHERE b.title LIKE ?1 ORDER BY b.price DESC")
  def findByTitleLike(title: String): java.util.List[Book]
}

Test-Controlで単体テストする

それでは、動作確認を兼ねてApache DeltaSpikeのTest-Controlでテストしてみましょう。

準備

まずは、Test-Control用のモジュールおよびCDIコンテナ制御の依存関係、JPA実装、テストライブラリが必要なので追加します。データベースは、H2とします。
JPAの実装はHibernateですが、このあとWildFlyで使う関係上、WildFly 10.0.0.Finalに含まれているHibernateと同じバージョンを使用しています。

        <!-- Unit Test -->
        <!-- Apache DeltaSpike Test-Control Module -->
        <dependency>
            <groupId>org.apache.deltaspike.modules</groupId>
            <artifactId>deltaspike-test-control-module-api</artifactId>
            <version>1.7.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.modules</groupId>
            <artifactId>deltaspike-test-control-module-impl</artifactId>
            <version>1.7.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.cdictrl</groupId>
            <artifactId>deltaspike-cdictrl-weld</artifactId>
            <version>1.7.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.weld.se</groupId>
            <artifactId>weld-se-core</artifactId>
            <version>2.3.5.Final</version>
            <scope>test</scope>
        </dependency>
        <!-- JPA Implementation for UnitTest -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.0.7.Final</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.192</version>
            <scope>test</scope>
        </dependency>
        <!-- UnitTest Library -->
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_${scala.major.version}</artifactId>
            <version>3.0.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

※「${scala.major.version}」は、「2.11」です

persistence.xml

先ほどのコード例では、Repositoryやエンティティは書きましたが、永続化ユニットの設定は書いていません。

単体テスト向けということで、こんなpersistence.xmlを用意。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
    <persistence-unit name="unit-test.persistence.unit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>
EntityManagerのProducerを作成する

続いて、EntityManagerをCDI管理BeanとするためのProducerを作成します。こちらは、ProjectStageがUnitTestで有効となるように実装しました。
src/main/scala/org/littlewings/javaee7/producer/EntityManagerProducers.scala

package org.littlewings.javaee7.producer

import javax.enterprise.context.{ApplicationScoped, Dependent}
import javax.enterprise.inject.Produces
import javax.persistence.{EntityManager, EntityManagerFactory, Persistence, PersistenceContext}

import org.apache.deltaspike.core.api.exclude.Exclude
import org.apache.deltaspike.core.api.projectstage.ProjectStage

object EntityManagerProducers {
  @Dependent
  @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.UnitTest]))
  class UnitTestEntityManagerProducer {
    @Produces
    @ApplicationScoped
    def entityManagerFactoryProducer: EntityManagerFactory = Persistence.createEntityManagerFactory("unit-test.persistence.unit")

    @Produces
    def entityManagerProducer(emf: EntityManagerFactory): EntityManager =
      emf.createEntityManager
  }
}

※あとでハマったのですが、@Excludeは@Producesに付けるのでは効かないことがわかり、こういう形になりました

テストコード

では、テストコードを書きます。

ざっくり、こんな感じで確認。
src/test/scala/org/littlewings/javaee7/repository/DeltaSpikeRepositoryTest.scala

package org.littlewings.javaee7.repository

import javax.inject.Inject
import javax.persistence.EntityManager

import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner
import org.junit.runner.RunWith
import org.junit.{Before, Test}
import org.littlewings.javaee7.entity.Book
import org.scalatest.Matchers
import org.scalatest.junit.JUnitSuite

@RunWith(classOf[CdiTestRunner])
class DeltaSpikeRepositoryTest extends JUnitSuite with Matchers {
  @Inject
  var bookRepository: BookRepository = _

  @Inject
  var em: EntityManager = _

  @Before
  def setUp(): Unit = {
    val tx = em.getTransaction
    tx.begin()
    em.createNativeQuery("TRUNCATE TABLE book").executeUpdate()
    tx.commit()
  }

  @Test
  def save(): Unit = {
    bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456))
    bookRepository.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104))
    bookRepository.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536))

    bookRepository.count should be(3L)
  }

  @Test
  def findByPrimaryKey(): Unit = {
    bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456))

    bookRepository.findBy("978-4774183169").price should be(3456)
  }

  @Test
  def update(): Unit = {
    val tx = em.getTransaction
    tx.begin()
    bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456))
    tx.commit()

    bookRepository.findBy("978-4774183169").price should be(3456)

    val tx2 = em.getTransaction
    tx2.begin()
    bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 4456))
    tx2.commit()

    bookRepository.findBy("978-4774183169").price should be(4456)
  }

  @Test
  def usingMethodExpressions(): Unit = {
    bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456))
    bookRepository.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104))
    bookRepository.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536))

    val resultBooks = bookRepository.findByPriceGreaterThanEqualsOrderByPriceDesc(4000)
    resultBooks should have size (2)
    resultBooks.get(0).isbn should be("978-4798124605") // Begging Java EE 6
    resultBooks.get(1).isbn should be("978-4798140926") // Java EE 7
  }

  @Test
  def usingQueryAnnotation(): Unit = {
    bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456))
    bookRepository.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104))
    bookRepository.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536))

    bookRepository.countPriceGreaterThan(4000) should be(2L)

    val resultBooks = bookRepository.findByTitleLike("%GlassFish%")
    resultBooks should have size (1)
    resultBooks.get(0).isbn should be("978-4798124605")
  }
}

テストの内容については、そう奇抜なことはしていないので、省略…。

実行は、「mvn test」で。

Arquillianを使ってインテグレーションテスト

では、最後にArquillianを使ってアプリケーションサーバー(ここではWildFly)にデプロイして確認してみます。

Java EE環境でやるということで、トランザクション管理はJTAの@Transactionalを使って管理したいと思います。

Apache DeltaSpikeにも@Transactionalというアノテーションがあるのですが、今回はこちらは置いておきます。
JPA Module

という条件を満たす実装、テストコードを書いてみましたよ、というお話です。

準備

ArquillianをWildFly Remoteで使うということと、デプロイの関係上Shrinkwrapが必要です。

Dependency Managementに、bomを追加。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.1.11.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.jboss.shrinkwrap</groupId>
                <artifactId>shrinkwrap-depchain</artifactId>
                <version>1.2.6</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

あとは依存関係にArquillianとWildFly Remote、ShrinkwrapのMaven Resolverを足していきます。

        <!-- Arquillian Integration Test -->
        <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.wildfly.arquillian</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <version>2.0.0.Final</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-api-maven</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-spi-maven</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-impl-maven-archive</artifactId>
            <scope>test</scope>
        </dependency>

参考)
Creating Deployable Archives with ShrinkWrap · Arquillian Guides
GitHub - shrinkwrap/resolver: ShrinkWrap Resolvers

インテグレーションテスト用のソースコードおよび設定は、「src/integration-test」配下に置くことにして、テスト対象は「**/*IT」とします。

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <version>1.12</version>
                <executions>
                    <execution>
                        <id>add-test-source</id>
                        <phase>generate-test-sources</phase>
                        <goals>
                            <goal>add-test-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>src/integration-test/scala</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.19.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <includes>
                        <include>**/*IT</include>
                    </includes>
                </configuration>
            </plugin>

Arquillianの設定としては、今回はServlet 3.0のみを指定しておきます。
src/integration-test/resources/aqruillian.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>

またWildFlyをダウンロード、展開し、ProjectStageをIntegrationTestとして起動しておきます。

$ wildfly-10.0.0.Final/bin/standalone.sh -Dorg.apache.deltaspike.ProjectStage=IntegrationTest
Serviceクラスの実装

@Transactionalを使うということで、先ほど作成したRepositoryを使ったServiceクラスを作成します。
src/main/scala/org/littlewings/javaee7/service/BookService.scala

package org.littlewings.javaee7.service

import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
import javax.transaction.Transactional

import org.littlewings.javaee7.entity.Book
import org.littlewings.javaee7.repository.BookRepository

@ApplicationScoped
class BookService {
  @Inject
  var bookRepository: BookRepository = _

  @Transactional
  def save(book: Book): Book = bookRepository.save(book)

  @Transactional
  def saveFail(book: Book): Book = {
    save(book)
    throw new RuntimeException("Oops!!")
  }

  @Transactional
  def findByIsbn(isbn: String): Book = bookRepository.findBy(isbn)

  @Transactional
  def countPriceGreaterThan(price: Int): Long = bookRepository.countPriceGreaterThan(price)

  @Transactional
  def findByTitleLike(title: String): java.util.List[Book] = bookRepository.findByTitleLike(title)
}

ロールバックを確認するためのメソッドも入れています。

DataSourceとpersistence.xmlとEntityManagerのProducer

インテグレーションテストでは、単体テストで使っていたH2はやめて、MySQLで確認することにします。

データベースについては、MySQL側に事前にこんなテーブルを作っておきます。

CREATE TABLE IF NOT EXISTS book(
  isbn VARCHAR(14),
  title VARCHAR(200),
  price INT(10),
  PRIMARY KEY(isbn)
);

データソースは、こんなスクリプトJDBCドライバのデプロイとともに、一気に作成。JDBCドライバは、MySQLのドライバがあらかじめMavenローカルリポジトリにあるものとします。
src/integration-test/resources/jboss-deploy-and-create-ds-script

connect
deploy ~/.m2/repository/mysql/mysql-connector-java/6.0.3/mysql-connector-java-6.0.3.jar
data-source add --name=mysqlDs --driver-name=mysql-connector-java-6.0.3.jar --driver-class=com.mysql.cj.jdbc.Driver --jndi-name=java:jboss/datasources/jdbc/mysqlDs --jta=true --connection-url=jdbc:mysql://localhost:3306/test --user-name=kazuhira --password=password
cd /subsystem=datasources/data-source=mysqlDs
./connection-properties=useUnicode:add(value=true)
./connection-properties=characterEncoding:add(value=utf-8)
./connection-properties=characterSetResults:add(value=utf-8)
./connection-properties=useServerPrepStmts:add(value=true)
./connection-properties=useLocalSessionState:add(value=true)
./connection-properties=elideSetAutoCommits:add(value=true)
./connection-properties=alwaysSendSetIsolation:add(value=false)
./connection-properties=useSSL:add(value=false)
reload

実行。

$ /path/to/wildfly-10.0.0.Final/bin/jboss-cli.sh --file=src/integration-test/resources/jboss-deploy-and-create-ds-script 

これで、データソースの作成まで完了です。

このデータソースをpersistence.xmlの永続化ユニットとして使うように定義し

    <persistence-unit name="integration-test.persistence.unit" transaction-type="JTA">
        <jta-data-source>java:jboss/datasources/jdbc/mysqlDs</jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL57InnoDBDialect"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="validate"/>
        </properties>
    </persistence-unit>

EntityManagerのProducer側にも追加します。

object EntityManagerProducers {
  @Dependent
  @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.IntegrationTest]))
  class IntegrationTestEntityManagerProducer {
    @PersistenceContext(unitName = "integration-test.persistence.unit")
    var entityManager: EntityManager = _

    @Produces
    @ApplicationScoped
    def entityManagerProducer: EntityManager = entityManager
  }

  @Dependent
  @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.UnitTest]))
  class UnitTestEntityManagerProducer {
    // 省略
  }
}
テストを書いて実行

では、これでArquillianを使ったテストを書いていきます。
src/integration-test/scala/org/littlewings/javaee7/service/ArquillianWithDeltaSpikeServiceIT.scala

package org.littlewings.javaee7.service

import javax.inject.Inject
import javax.persistence.EntityManager
import javax.transaction.UserTransaction

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.runner.RunWith
import org.junit.{Before, Test}
import org.littlewings.javaee7.entity.Book
import org.scalatest.Matchers
import org.scalatest.junit.JUnitSuite

@RunWith(classOf[Arquillian])
class ArquillianWithDeltaSpikeServiceIT extends JUnitSuite with Matchers {
  @Inject
  var userTransaction: UserTransaction = _

  @Inject
  var em: EntityManager = _

  @Inject
  var bookService: BookService = _

  @Before
  def setUp(): Unit = {
    userTransaction.begin()
    em.createNativeQuery("TRUNCATE TABLE book").executeUpdate()
    userTransaction.commit()
  }

  @Test
  def save(): Unit = {
    bookService.save(Book("978-4774183169", "パーフェクト Java EE", 3456))
    bookService.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104))
    bookService.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536))

    bookService.findByIsbn("978-4774183169").price should be(3456)
  }

  @Test
  def rollback(): Unit = {
    val thrown = the[RuntimeException] thrownBy bookService.saveFail(Book("978-4774183169", "パーフェクト Java EE", 3456))
    thrown.getMessage should be("Oops!!")

    bookService.findByIsbn("978-4774183169") should be(null)
  }

  @Test
  def usingQueryAnnotation(): Unit = {
    bookService.save(Book("978-4774183169", "パーフェクト Java EE", 3456))
    bookService.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104))
    bookService.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536))

    val resultBooks = bookService.findByTitleLike("%徹底入門%")
    resultBooks should have size(1)
    resultBooks.get(0).isbn should be("978-4798140926")
  }
}

object ArquillianWithDeltaSpikeServiceIT {
  @Deployment
  def createDeployment: WebArchive = {
    ShrinkWrap
      .create(classOf[WebArchive])
      .addPackages(true, "org.littlewings.javaee7")
      .addAsResource("META-INF/apache-deltaspike.properties", "META-INF/apache-deltaspike.properties")
      .addAsResource("META-INF/beans.xml", "META-INF/beans.xml")
      .addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml")
      .addAsLibraries(
        Maven
          .resolver
          .loadPomFromFile("pom.xml")
          .importRuntimeDependencies
          .resolve("org.scalatest:scalatest_2.11:3.0.0")
          .withTransitivity
          .asFile: _*
      )
  }
}

ではこれで、テストを実行。

$ mvn test-compile failsafe:integration-test

単体テストのコードは動かしたくなかったので、この指定…

ところが、これはうまくいきません。

このコードをそのまま動かすと、以下のようなエラーを見ることになります。

java.lang.IllegalStateException: A JTA EntityManager cannot use getTransaction()

これは、Apache DeltaSpikeのData Moduleが依存する、JPA ModuleがRESOURCE_LOCALを想定していることが理由です。

参考)
DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(2) | なるほど!ザ・Weld

これを回避するためには、META-INF/apache-deltaspile.propertiesに以下のような記述をします。
src/main/resources/META-INF/apache-deltaspike.properties

globalAlternatives.org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa.impl.transaction.ContainerManagedTransactionStrategy

コンテナ管理のトランザクション制御を利用するようにしましょう、と。TransactionStrategyインターフェースの実装で選択するのですが、以下のパッケージに実装があります。
https://github.com/apache/deltaspike/tree/deltaspike-1.7.1/deltaspike/modules/jpa/impl/src/main/java/org/apache/deltaspike/jpa/impl/transaction

デフォルトはResourceLocalTransactionStrategyで、その他にBeanManagedUserTransactionStrategy、ContainerManagedTransactionStrategy、EnvironmentAwareTransactionStrategyがありますが、今回の目的で使えるのはContainerManagedTransactionStrategyだけです。

また、ドキュメントの方には、beans.xmlにalternativesを書けばいいよと書かれていましたが、うまく動作しませんでした…。

この設定を入れると、テストをパスするようになります。

めでたし、めでたし。

単体テストはどうした

ですが、ここで「src/main/resources/META-INF/apache-deltaspike.properties」にTransactionStrategyの設定を書いてしまったので、実は単体テスト側の動作を破壊したことになります。

さて、どうしましょう?

「src/main/resources/META-INF/apache-deltaspike.properties」の内容は変えないとして、単体テストではResourceLocalTransactionStrategyを使いたいわけです。また、globalAlternativesについてはProjectStageは見てくれません。

今回は、別の設定ファイルを追加して、apache-deltaspike.propertiesより優先度の高いファイルを作り、その内容を上書きする方法で対処しました。

次のようなクラスを作成します。追加するファイルは、「unit-test-apache-deltaspike.properties」とします。
src/test/scala/org/littlewings/javaee7/repository/UnitTestApacheDeltaSpikePropertyFileConfig.scala

package org.littlewings.javaee7.repository

import org.apache.deltaspike.core.api.config.PropertyFileConfig

class UnitTestApacheDeltaSpikePropertyFileConfig extends PropertyFileConfig {
  override def getPropertyFileName: String = "META-INF/unit-test-apache-deltaspike.properties"

  override def isOptional: Boolean = false
}

このクラスをService Providerの仕組みに乗せます。
src/test/resources/META-INF/services/org.apache.deltaspike.core.api.config.PropertyFileConfig

org.littlewings.javaee7.repository.UnitTestApacheDeltaSpikePropertyFileConfig

「unit-test-apache-deltaspike.properties」の内容は、以下のように書きます。
src/test/resources/META-INF/unit-test-apache-deltaspike.properties

deltaspike_ordinal=110
globalAlternatives.org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy

TransactionStrategyをResourceLocalTransactionStrategyにするのはもちろんですが、deltaspike_ordinalを100よりも大きくすることで、オリジナルのapache-deltaspile.propertiesよりも優先度を上にします。
Providing configuration using ConfigSources

Propertiesファイルの優先度は100で、値が大きいほど優先度が高くなります。システムプロパティが最高で、400になっています。

今回は、これで単体テストではResourceLocalTransactionStrategyを使い、インテグレーションテストではContainerManagedTransactionStrategyを使うようにしました。

まとめ

Apache DeltaSpikeのData Moduleを使い、JPAのクエリ実行を試しつつ、Test-Controlで単体テスト、ArquillianでインテグレーションテストをしてJTAとの統合まで確認してみました。

Data Module自体より、ProjectStageとTransactionStrategyにとてもハマったのですが、なんとか目標にしていた形には持っていけました。

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

参考)
DeltaSpike Dataの紹介 | なるほど!ザ・Weld
DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(1) | なるほど!ザ・Weld
DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(2) | なるほど!ザ・Weld
Creating Deployable Archives with ShrinkWrap · Arquillian Guides
GitHub - shrinkwrap/resolver: ShrinkWrap Resolvers
build - Prevent unit tests in maven but allow integration tests - Stack Overflow