これは、なにをしたくて書いたもの?
このブログではJakarta EE(Java EE)環境ではないところではデータベースアクセスにDomaをよく使っているのですが、そういえば
Jakarta EE(Java EE)のアプリケーションサーバーで使うように設定したことがないな、と思いまして。
1度やってみることにしました。デプロイするアプリケーションサーバーはWildFly 34.0.1.Finalとします。
ついでにArquillianを使ったテストも書いてみます。
DomaをCDIと組み合わせる
DomaをJakarta Contexts and Dependency Injection(以降CDI)と組み合わせるには、Domaのドキュメントのこちらのページを参考にします。
今回はDaoとConfigをCDI管理Beanとすることを考えます。
Daoの実装クラスに付与するアノテーションを制御するには、AnnotateWith
とAnnotation
の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>
ひとまず、テストで必要なもの以外を並べています。
<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:dev
やwildfly: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:run
やwildfly: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
を使ってもいいかもしれません。
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
<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を使ったアプリケーションをデプロイしてみました。
DomaとCDIの組み合わせにはそこまで苦労しませんでしたが、Arquillianでテストを書くところにとても時間がかかりました…。
いつまで経ってもArquillianには慣れない気がしますが、テストコードとしては書くようにしたいなと思っていたりします。