CLOVER🍀

That was when it all began.

WildFly 34にCDIと組み合わせたDoma 3を使ったアプリケーションをデプロイする(Arquillianでのインテグレーションテスト付き)

これは、なにをしたくて書いたもの?

このブログではJakarta EE(Java EE)環境ではないところではデータベースアクセスにDomaをよく使っているのですが、そういえば
Jakarta EE(Java EE)のアプリケーションサーバーで使うように設定したことがないな、と思いまして。

1度やってみることにしました。デプロイするアプリケーションサーバーはWildFly 34.0.1.Finalとします。

ついでにArquillianを使ったテストも書いてみます。

DomaCDIと組み合わせる

DomaJakarta Contexts and Dependency Injection(以降CDI)と組み合わせるには、Domaのドキュメントのこちらのページを参考にします。

設定 / 設定クラスの定義 / 高度な定義

今回はDaoとConfigをCDI管理Beanとすることを考えます。

Daoの実装クラスに付与するアノテーションを制御するには、AnnotateWithAnnotationの2つのアノテーションを使います。

ドキュメントの例はこちらです。

@Dao
@AnnotateWith(annotations = {
    @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = javax.inject.Inject.class),
    @Annotation(target = AnnotationTarget.CONSTRUCTOR_PARAMETER, type = javax.inject.Named.class, elements = "\"config\"") })
public interface EmployeeDao {

    @Select
    Employee selectById(Integer id);
}

ここで、ConfigをあらかじめCDI管理Beanとして定義しておいて

@ApplicationScoped
public class DomaConfig implements Config {
    ...
}

DaoをCDI管理Beanとして定義しつつ、Config`をインジェクションするにはこんな感じで書きます。

@Dao
@AnnotateWith(annotations = {
    @Annotation(target = AnnotationTarget.CLASS, type = ApplicationScoped.class),
    @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})
public interface EmployeeDao {

    @Select
    Employee selectById(Integer id);
}

生成されるDaoの実装クラスはこんなイメージになります。

@ApplicationScoped
public class EmployeeDaoImpl {
    @Inject
    public EmployeeDaoImpl(Config config) {
        ...
    }

    ....
}

なお、以下のアノテーションの定義は別のアノテーションにまとめることもできます。@Daoアノテーションはその中には含められません。

@AnnotateWith(annotations = {
    @Annotation(target = AnnotationTarget.CLASS, type = ApplicationScoped.class),
    @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})

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

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.5, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-49-generic", arch: "amd64", family: "unix"

データベースにはMySQLを使います。MySQLには172.17.0.2でアクセスできるものとします。

 MySQL  localhost:3306 ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.3     |
+-----------+
1 row in set (0.0187 sec)

Domaを使ったアプリケーションを書く

まずはDomaを使ったアプリケーションを書いていきます。

テーブル定義。お題は書籍にします。

create table book(
  isbn varchar(14),
  title varchar(100),
  price int,
  publish_date date,
  primary key(isbn)
);

Maven依存関係など。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>wildfly-ee-with-tools</artifactId>
                <version>34.0.1.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.11.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.ws.rs</groupId>
            <artifactId>jakarta.ws.rs-api</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.transaction</groupId>
            <artifactId>jakarta.transaction-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-core</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.seasar.doma</groupId>
                            <artifactId>doma-processor</artifactId>
                            <version>3.1.0</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

ひとまず、テストで必要なもの以外を並べています。

Domaに関するものはdoma-coreと

        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-core</artifactId>
            <version>3.1.0</version>
        </dependency>

Pluggable Annotation Processing APIの設定ですね。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.seasar.doma</groupId>
                            <artifactId>doma-processor</artifactId>
                            <version>3.1.0</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

DomaのConfigクラスの実装。CDI管理Beanとして定義します。

src/main/java/org/littlewings/wildfly/doma/config/DomaConfig.java

package org.littlewings.wildfly.doma.config;

import javax.sql.DataSource;

import jakarta.annotation.Resource;
import jakarta.enterprise.context.ApplicationScoped;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.MysqlDialect;

@ApplicationScoped
public class DomaConfig implements Config {
    @Resource(name = "java:jboss/datasources/MySqlDs")
    private DataSource dataSource;

    private Dialect dialect = new MysqlDialect();

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public Naming getNaming() {
        return Naming.SNAKE_LOWER_CASE;
    }
}

DataSourceはJNDIでインジェクションしていますが、この定義はまた後で。

エンティティ。

src/main/java/org/littlewings/wildfly/doma/entity/Book.java

package org.littlewings.wildfly.doma.entity;

import java.time.LocalDate;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public record Book(
        @Id
        String isbn,
        String title,
        Integer price,
        LocalDate publishDate
) {
}

Dao。

src/main/java/org/littlewings/wildfly/doma/dao/BookDao.java

package org.littlewings.wildfly.doma.dao;

import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.littlewings.wildfly.doma.entity.Book;
import org.seasar.doma.AnnotateWith;
import org.seasar.doma.Annotation;
import org.seasar.doma.AnnotationTarget;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Sql;
import org.seasar.doma.Update;
import org.seasar.doma.jdbc.Result;

@Dao
@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = ApplicationScoped.class),
        @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})
public interface BookDao {
    @Select
    Book findByIsbn(String isbn);

    @Select
    List<Book> findAllByPriceAsc();

    @Insert
    Result<Book> insert(Book book);

    @Update
    Result<Book> update(Book book);

    @Delete
    Result<Book> delete(Book book);
}

この定義でDaoの実装クラスがCDI管理Beanになり、実装したConfigのインスタンスがインジェクションされます。

@Dao
@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = ApplicationScoped.class),
        @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})
public interface BookDao {

説明に書いたように、こんな感じで@AnnotateWith@Annotationアノテーションをまとめたアノテーションを作成して

src/main/java/org/littlewings/wildfly/doma/dao/WithCdi.java

package org.littlewings.wildfly.doma.dao;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.seasar.doma.AnnotateWith;
import org.seasar.doma.Annotation;
import org.seasar.doma.AnnotationTarget;

@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = ApplicationScoped.class),
        @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})
@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WithCdi {
}

これをDaoに付与してもOKです。

@Dao
@WithCdi
public interface BookDao {

Daoに対応するSQLファイル。@Sqlで定義してもよかったのですが、今回はこちらの構成にしました。

src/main/resources/META-INF/org/littlewings/wildfly/doma/dao/BookDao/findByIsbn.sql

select
  /*%expand*/*
from
  book
where
  isbn = /* isbn */'abcde'

src/main/resources/META-INF/org/littlewings/wildfly/doma/dao/BookDao/findAllByPriceAsc.sql

select
  /*%expand*/*
from
  book
order by
  price asc

Jakarta RESTful Web Services(以降JAX-RS)リソースクラス。

src/main/java/org/littlewings/wildfly/doma/resource/BooksResource.java

package org.littlewings.wildfly.doma.resource;

import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.littlewings.wildfly.doma.dao.BookDao;
import org.littlewings.wildfly.doma.entity.Book;

@Path("/books")
@ApplicationScoped
@Transactional
public class BooksResource {
    @Inject
    private BookDao bookDao;

    @GET
    @Path("/{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Book findByIsbn(@PathParam("isbn") String isbn) {
        return bookDao.findByIsbn(isbn);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Book> findAll() {
        return bookDao.findAllByPriceAsc();
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public void register(Book book) {
        bookDao.insert(book);
    }

    @PUT
    @Path("/{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    public void update(@PathParam("isbn") String isbn, Book book) {
        Book registeredBook = bookDao.findByIsbn(isbn);

        if (registeredBook == null) {
            return;
        }

        if (!isbn.equals(book.isbn())) {
            return;
        }

        bookDao.update(book);
    }

    @DELETE
    @Path("/{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    public void delete(@PathParam("isbn") String isbn) {
        Book book = bookDao.findByIsbn(isbn);

        if (book != null) {
            bookDao.delete(book);
        }
    }
}

JAX-RSの有効化。

src/main/java/org/littlewings/wildfly/doma/RestApplication.java

package org.littlewings.wildfly.doma;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/")
public class RestApplication extends Application {
}

WildFlyをプロビジョニングする

WildFlyへのデプロイは、WildFly Maven Pluginで行うことにします。デプロイというか、プロビジョニングですね。

            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>5.0.1.Final</version>
                <executions>
                    <execution>
                        <id>package</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <overwrite-provisioned-server>true</overwrite-provisioned-server>
                    <discover-provisioning-info>
                        <version>34.0.1.Final</version>
                        <add-ons>
                            <add-on>mysql</add-on>
                        </add-ons>
                    </discover-provisioning-info>
                    <env>
                        <MYSQL_DATASOURCE>MySqlDs</MYSQL_DATASOURCE>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </env>
                </configuration>
            </plugin>

MySQLのデータソースは、Galleon Feature Packs for integrating datasources into WildFly and WildFly Previewを使って定義します。

GitHub - wildfly-extras/wildfly-datasources-galleon-pack: WildFly Feature Pack for DataSources

WildFly Glowのアドオンとしてはmysqlになりますね。

                        <add-ons>
                            <add-on>mysql</add-on>
                        </add-ons>

設定は環境変数で行います。ここでの設定はwildfly:devwildfly:runゴールやテストで動かす時に使います。

                    <env>
                        <MYSQL_DATASOURCE>MySqlDs</MYSQL_DATASOURCE>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </env>

それぞれの環境変数の意味はこちらを参照。

https://github.com/wildfly-extras/wildfly-datasources-galleon-pack/tree/9.0.0.Final/doc/mysql

MYSQL_DATASOURCE環境変数は、データソース名の一部になります。

デフォルトだとjava:jboss/datasources/${MYSQL_DATASOURCE}というJNDI名になるので、Configで指定していたJNDI名の前提はこういう
背景になっています。

@ApplicationScoped
public class DomaConfig implements Config {
    @Resource(name = "java:jboss/datasources/MySqlDs")
    private DataSource dataSource;

ちなみに各環境変数の値がプロパティになっていますが、今後のテストのことがあってpropertiesに定義しておきました。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <jdbc.url><![CDATA[jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin]]></jdbc.url>
        <jdbc.user>kazuhira</jdbc.user>
        <jdbc.password>password</jdbc.password>
    </properties>

では、確認してみましょう。先に書いたように、今回はwildfly:runゴールで起動させます。

$ mvn wildfly:run


## 以下でも可
$ mvn compile wildfly:dev

packageでプロビジョニングしたWildFlyを起動してもいいのですが、その場合はMySQLへの接続設定をまた環境変数で行うことになるので
ちょっと面倒です。

wildfly:runwildfly:devだと今回の設定のpom.xmlに書いた環境変数の値で起動します。

簡単に確認。

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/books -d '{"isbn": "978-4621303252", "title": "Effective Java 第3版", "price": 4400, "publishDate": "2018-10-30"}'


$ curl localhost:8080/books/978-4621303252
{"isbn":"978-4621303252","price":4400,"publishDate":"2018-10-30","title":"Effective Java 第3版"}


$ curl localhost:8080/books
[{"isbn":"978-4621303252","price":4400,"publishDate":"2018-10-30","title":"Effective Java 第3版"}]

その他のパターンは、テストで確認します。

ちなみに、この設定だとDaoを使う度にSQLログが出力されます。

23:09:22,876 INFO  [org.seasar.doma.jdbc.UtilLoggingJdbcLogger] (default task-1) [DOMA2076] SQL LOG : PATH=[META-INF/org/littlewings/wildfly/doma/dao/BookDao/findByIsbn.sql],
select
  isbn, title, price, publish_date
from
  book
where
  isbn = '978-4621303252'

これが気になる場合は、UtilLoggingJdbcLoggerのログレベルを下げるとよいでしょう。

@ApplicationScoped
public class DomaConfig implements Config {
    @Resource(name = "java:jboss/datasources/MySqlDs")
    private DataSource dataSource;

    private Dialect dialect = new MysqlDialect();

    private JdbcLogger jdbcLogger = new UtilLoggingJdbcLogger(Level.FINE);

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public Naming getNaming() {
        return Naming.SNAKE_LOWER_CASE;
    }

    @Override
    public JdbcLogger getJdbcLogger() {
        return jdbcLogger;
    }
}

ログレベルがわかりにくかったら、Slf4jJdbcLoggerを使ってもいいかもしれません。

SLF4J サポート — Doma ドキュメント

Slf4jJdbcLoggerを使った場合、Logbackは不要です。どのログライブラリーを使った場合でも、WildFlyにデプロイした場合は
JBoss LogManagerにすべて流れるようになっています。

Arquillianでインテグレーションテストを書く

最後はテストコードで確認します。Arquillianを使ったインテグレーションテストを書きましょう。

テストライブラリーを依存関係に追加。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.26.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>5.5.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.18.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.18.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.1.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.junit5</groupId>
            <artifactId>arquillian-junit5-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.wildfly.arquillian</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-depchain</artifactId>
            <version>3.3.2</version>
            <scope>test</scope>
            <type>pom</type>
        </dependency>

ArquillianはRemoteで使います。MySQLのConnector/Jがあらためて依存関係に入っているのは、テストコードで事前にデータの削除を
行うためです。

またArquillianでデプロイするアーカイブDomaを含めるためShrinkwrap Resolverも使います。

GitHub - shrinkwrap/resolver: ShrinkWrap Resolvers

Mavenプラグインの設定は、こうなりました。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.5.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <environmentVariables>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </environmentVariables>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>5.0.1.Final</version>
                <executions>
                    <execution>
                        <id>package</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>start-before-integration-test</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>shutdown-after-integration-test</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>shutdown</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <overwrite-provisioned-server>true</overwrite-provisioned-server>
                    <discover-provisioning-info>
                        <version>34.0.1.Final</version>
                        <add-ons>
                            <add-on>mysql</add-on>
                        </add-ons>
                    </discover-provisioning-info>
                    <env>
                        <MYSQL_DATASOURCE>MySqlDs</MYSQL_DATASOURCE>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </env>
                </configuration>
            </plugin>

Maven Failsafe Pluginを追加してインテグレーションテストが実行できるようにしているのと、環境変数を設定してクライアント側の
テストコードからJDBC接続情報を参照できるようにしています。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.5.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <environmentVariables>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </environmentVariables>
                </configuration>
            </plugin>

WildFly Maven Pluginではインテグレーションテストの前後でWildFlyの起動と停止を行うようにしました。

            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>5.0.1.Final</version>
                <executions>
                    <execution>
                        <id>package</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>start-before-integration-test</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>shutdown-after-integration-test</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>shutdown</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <overwrite-provisioned-server>true</overwrite-provisioned-server>
                    <discover-provisioning-info>
                        <version>34.0.1.Final</version>
                        <add-ons>
                            <add-on>mysql</add-on>
                        </add-ons>
                    </discover-provisioning-info>
                    <env>
                        <MYSQL_DATASOURCE>MySqlDs</MYSQL_DATASOURCE>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </env>
                </configuration>
            </plugin>

なお、wildfly:startゴールではプロジェクトのアーティファクトをデプロイしない状態のWildFlyが起動します。

テストコードはこちら。

Daoのテスト。

src/test/java/org/littlewings/wildfly/doma/BookDaoIT.java

package org.littlewings.wildfly.doma;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;
import javax.sql.DataSource;

import jakarta.annotation.Resource;
import jakarta.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.littlewings.wildfly.doma.dao.BookDao;
import org.littlewings.wildfly.doma.entity.Book;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
class BookDaoIT {
    @Resource(name = "java:jboss/datasources/MySqlDs")
    private DataSource dataSource;

    @Inject
    private BookDao bookDao;

    @Deployment
    static WebArchive createDeployment() throws IOException {
        File[] compileAndRuntimeScopeDependencyFiles =
                Maven
                        .resolver()
                        .loadPomFromFile("pom.xml")
                        .importCompileAndRuntimeDependencies()
                        .resolve()
                        .withTransitivity()
                        .asFile();

        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap
                .create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "")
                .addAsLibraries(compileAndRuntimeScopeDependencyFiles)
                .addAsLibraries(
                        Maven
                                .resolver()
                                .resolve("org.assertj:assertj-core:3.26.3")
                                .withTransitivity()
                                .asFile()
                );
    }

    @BeforeEach
    void setUp() throws SQLException {
        try (Connection connection = dataSource.getConnection();
             PreparedStatement ps = connection.prepareStatement("truncate table book")) {
            ps.executeUpdate();
        }
    }

    @Test
    void test() {
        Book effectiveJava = new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30));
        Book testingJava = new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3));
        Book gettingStartedJava = new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18));

        // 登録
        for (Book book : List.of(effectiveJava, testingJava, gettingStartedJava)) {
            bookDao.insert(book);
        }

        // 全件取得
        assertThat(bookDao.findAllByPriceAsc())
                .hasSize(3)
                .isEqualTo(List.of(gettingStartedJava, testingJava, effectiveJava));

        // 1件取得
        assertThat(bookDao.findByIsbn(effectiveJava.isbn()))
                .isEqualTo(effectiveJava);

        // 削除
        bookDao.delete(gettingStartedJava);

        assertThat(bookDao.findAllByPriceAsc())
                .hasSize(2)
                .isEqualTo(List.of(testingJava, effectiveJava));

        // 更新
        Book priceDownEffectiveJava = new Book("978-4621303252", "Effective Java 第3版", 3000, LocalDate.of(2018, 10, 30));
        bookDao.update(priceDownEffectiveJava);

        assertThat(bookDao.findByIsbn(priceDownEffectiveJava.isbn()))
                .isEqualTo(priceDownEffectiveJava);
    }
}

テストの開始前にデータを削除するため、DataSourceをJNDIで取得してインジェクションしています。

    @Resource(name = "java:jboss/datasources/MySqlDs")
    private DataSource dataSource;


    @BeforeEach
    void setUp() throws SQLException {
        try (Connection connection = dataSource.getConnection();
             PreparedStatement ps = connection.prepareStatement("truncate table book")) {
            ps.executeUpdate();
        }
    }

デプロイするアーカイブDomaがあるのでcompileおよびruntimeスコープの依存関係を含みつつ、src/main/resources配下の
リソース一式も含めます。最後にテストで使うAssertJも含めておきます。

    @Deployment
    static WebArchive createDeployment() throws IOException {
        File[] compileAndRuntimeScopeDependencyFiles =
                Maven
                        .resolver()
                        .loadPomFromFile("pom.xml")
                        .importCompileAndRuntimeDependencies()
                        .resolve()
                        .withTransitivity()
                        .asFile();

        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap
                .create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "")
                .addAsLibraries(compileAndRuntimeScopeDependencyFiles)
                .addAsLibraries(
                        Maven
                                .resolver()
                                .resolve("org.assertj:assertj-core:3.26.3")
                                .withTransitivity()
                                .asFile()
                );
    }

ここでのポイントはこのあたりですね。

続いてJAX-RSリソースクラスのテストコード。こちらはクライアントとして動作します。

src/test/java/org/littlewings/wildfly/doma/BooksResourceIT.java

package org.littlewings.wildfly.doma;

import java.io.File;
import java.net.URL;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef;
import io.restassured.config.ObjectMapperConfig;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit5.ArquillianExtension;
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.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.littlewings.wildfly.doma.entity.Book;

import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
@RunAsClient
class BooksResourceIT {
    @ArquillianResource
    private URL deploymentUrl;

    private String resourcePrefix =
            RestApplication.class
                    .getAnnotation(ApplicationPath.class)
                    .value()
                    .replaceFirst("^/", "");

    @Deployment
    static WebArchive createDeployment() {
        File[] compileAndRuntimeScopeDependencyFiles =
                Maven
                        .resolver()
                        .loadPomFromFile("pom.xml")
                        .importCompileAndRuntimeDependencies()
                        .resolve()
                        .withTransitivity()
                        .asFile();

        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap
                .create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "")
                .addAsLibraries(compileAndRuntimeScopeDependencyFiles);
    }

    @BeforeEach
    void setUp() throws SQLException {
        RestAssured.baseURI = deploymentUrl + resourcePrefix;
        RestAssured.config = RestAssured.config().objectMapperConfig(ObjectMapperConfig.objectMapperConfig().jackson2ObjectMapperFactory(
                (cls, charset) -> new ObjectMapper()
                        .findAndRegisterModules()
                        .setDateFormat(new StdDateFormat())
                        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        ));

        try (Connection connection = DriverManager.getConnection(System.getenv("MYSQL_URL"), System.getenv("MYSQL_USER"), System.getenv("MYSQL_PASSWORD"));
             PreparedStatement ps = connection.prepareStatement("truncate table book")) {
            ps.executeUpdate();
        }
    }

    @Test
    void test() {
        Book effectiveJava = new Book("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30));
        Book testingJava = new Book("978-4297144357", "Javaエンジニアのための ソフトウェアテスト実践入門 ~自動化と生成AIによるモダンなテスト技法~", 3520, LocalDate.of(2024, 10, 3));
        Book gettingStartedJava = new Book("978-4774189093", "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18));

        // 登録
        for (Book book : List.of(effectiveJava, testingJava, gettingStartedJava)) {
            given()
                    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
                    .body(book)
                    .when()
                    .post("/books")
                    .then()
                    .statusCode(Response.Status.NO_CONTENT.getStatusCode());
        }

        // 全件取得
        List<Book> allBooks =
                given()
                        .when()
                        .get("/books")
                        .then()
                        .statusCode(Response.Status.OK.getStatusCode())
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(new TypeRef<>() {
                        });

        assertThat(allBooks)
                .isEqualTo(List.of(gettingStartedJava, testingJava, effectiveJava));

        // 1件取得
        Book foundBook =
                given()
                        .pathParams("isbn", effectiveJava.isbn())
                        .when()
                        .get("/books/{isbn}")
                        .then()
                        .statusCode(Response.Status.OK.getStatusCode())
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(Book.class);

        assertThat(foundBook).isEqualTo(effectiveJava);

        // 削除
        given()
                .pathParams("isbn", gettingStartedJava.isbn())
                .when()
                .delete("/books/{isbn}")
                .then()
                .statusCode(Response.Status.NO_CONTENT.getStatusCode());

        // 全件取得
        List<Book> allBooks2 =
                given()
                        .when()
                        .get("/books")
                        .then()
                        .statusCode(Response.Status.OK.getStatusCode())
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(new TypeRef<>() {
                        });

        assertThat(allBooks2)
                .isEqualTo(List.of(testingJava, effectiveJava));

        // 更新
        Book priceDownEffectiveJava = new Book("978-4621303252", "Effective Java 第3版", 3000, LocalDate.of(2018, 10, 30));

        given()
                .pathParams("isbn", priceDownEffectiveJava.isbn())
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
                .body(priceDownEffectiveJava)
                .when()
                .put("/books/{isbn}")
                .then()
                .statusCode(Response.Status.NO_CONTENT.getStatusCode());

        // 1件取得
        Book foundBook2 =
                given()
                        .pathParams("isbn", priceDownEffectiveJava.isbn())
                        .when()
                        .get("/books/{isbn}")
                        .then()
                        .statusCode(Response.Status.OK.getStatusCode())
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(Book.class);

        assertThat(foundBook2).isEqualTo(priceDownEffectiveJava);
    }
}

デプロイするアーカイブは、testスコープのものを含めなくていいので少しだけ単純になっています。

    @Deployment
    static WebArchive createDeployment() {
        File[] compileAndRuntimeScopeDependencyFiles =
                Maven
                        .resolver()
                        .loadPomFromFile("pom.xml")
                        .importCompileAndRuntimeDependencies()
                        .resolve()
                        .withTransitivity()
                        .asFile();

        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap
                .create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "")
                .addAsLibraries(compileAndRuntimeScopeDependencyFiles);
    }

データの削除は、WildFlyに登録してあるDataSourceを使えないので自分でJDBC接続を開いています。

    @BeforeEach
    void setUp() throws SQLException {
        RestAssured.baseURI = deploymentUrl + resourcePrefix;
        RestAssured.config = RestAssured.config().objectMapperConfig(ObjectMapperConfig.objectMapperConfig().jackson2ObjectMapperFactory(
                (cls, charset) -> new ObjectMapper()
                        .findAndRegisterModules()
                        .setDateFormat(new StdDateFormat())
                        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        ));

        try (Connection connection = DriverManager.getConnection(System.getenv("MYSQL_URL"), System.getenv("MYSQL_USER"), System.getenv("MYSQL_PASSWORD"));
             PreparedStatement ps = connection.prepareStatement("truncate table book")) {
            ps.executeUpdate();
        }
    }

Maven Failsafe Pluginで環境変数を定義して、内容はプロパティとして設定したのはこのためですね。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.5.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <environmentVariables>
                        <MYSQL_URL>${jdbc.url}</MYSQL_URL>
                        <MYSQL_USER>${jdbc.user}</MYSQL_USER>
                        <MYSQL_PASSWORD>${jdbc.password}</MYSQL_PASSWORD>
                    </environmentVariables>
                </configuration>
            </plugin>

あとJacksonのObjectMapperのカスタマイズも行っています。

        RestAssured.config = RestAssured.config().objectMapperConfig(ObjectMapperConfig.objectMapperConfig().jackson2ObjectMapperFactory(
                (cls, charset) -> new ObjectMapper()
                        .findAndRegisterModules()
                        .setDateFormat(new StdDateFormat())
                        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        ));

あとはmvn verifyを実行すると、WildFlyをプロビジョニングしてインテグレーションテストを行います。

$ mvn verify

## または
$ mvn integration-test

インテグレーションテストの前後には、プロビジョニングしたWildFlyの起動と停止が行われます。

こんなところでしょうか。

オマケ

インテグレーションテストで毎回WildFlyの起動と停止をしていると重たいので、以下でアーティファクトをデプロイしていないWildFly
プロビジョニングして起動していました。

$ mvn package -DskipTests=true -Dwildfly.package.deployment.skip=true && mvn wildfly:start

テストを実行する際には、WildFlyに関するゴールをスキップして実行しています。

$ mvn verify -Dwildfly.skip -Dwildfly.package.skip

## または
$ mvn integration-test -Dwildfly.skip -Dwildfly.package.skip

こうするとWildFlyの起動・停止の分は短縮されるので気軽にテストを実行できるようになります。

IDE上でテストクラスを直接実行しても、Mavenのライフサイクルに沿ってWildFlyの起動・停止は行われないので、ちょうどいい感じで
扱えました。

おわりに

WildFly 34に、CDIと組み合わせたDoma 3を使ったアプリケーションをデプロイしてみました。

DomaCDIの組み合わせにはそこまで苦労しませんでしたが、Arquillianでテストを書くところにとても時間がかかりました…。

いつまで経ってもArquillianには慣れない気がしますが、テストコードとしては書くようにしたいなと思っていたりします。