CLOVER🍀

That was when it all began.

Jakarta Dataを使ったアプリケーションをArquillianでテストする

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

こちらのエントリーの続きです。

Jakarta EE 11で追加されたJakarta Data 1.0を試す - CLOVER🍀

一緒にArquillianを使ったテストコードも書いていたのですが、エントリーがあまりに長くなったので分割することにしました。

Jakarta DataとArquillian

今回、ArquillianはWildFly Arquillian AdaptorのRemoteで使うことにします。

それくらいで困ることはないかなと思っていましたが、リポジトリーインターフェースに対するテストを書こうとすると
意外とハマりました。

Jakarta Persistence(JPA)を使う場合も同じかもしれませんが。

リポジトリーインターフェースを使ってdeleteなどを実行するテストを書こうとすると、こんな感じで
「アクティブなトランザクションがない」と言われてテストに失敗します。

IdentifiedTestException [collectedExceptions={[engine:junit-jupiter]/[class:org.littlewings.jakarta.data.book.BookRepositoryIT]/[method:basicRepository()]=jakarta.data.exceptions.DataException: No active transaction for update or delete query}]
        at deployment.6141daa7-f824-4a3d-ae38-924a7eb0cd3c.war//org.jboss.arquillian.junit5.container.JUnitJupiterTestRunner$ArquillianTestMethodExecutionListener.getTestResult(JUnitJupiterTestRunner.java:113)
        at deployment.6141daa7-f824-4a3d-ae38-924a7eb0cd3c.war//org.jboss.arquillian.junit5.container.JUnitJupiterTestRunner$ArquillianTestMethodExecutionListener.access$100(JUnitJupiterTestRunner.java:72)
        at deployment.6141daa7-f824-4a3d-ae38-924a7eb0cd3c.war//org.jboss.arquillian.junit5.container.JUnitJupiterTestRunner.execute(JUnitJupiterTestRunner.java:61)
        at deployment.6141daa7-f824-4a3d-ae38-924a7eb0cd3c.war//org.jboss.arquillian.protocol.servlet5.runner.ServletTestRunner.executeTest(ServletTestRunner.java:139)
        at deployment.6141daa7-f824-4a3d-ae38-924a7eb0cd3c.war//org.jboss.arquillian.protocol.servlet5.runner.ServletTestRunner.execute(ServletTestRunner.java:117)
        at deployment.6141daa7-f824-4a3d-ae38-924a7eb0cd3c.war//org.jboss.arquillian.protocol.servlet5.runner.ServletTestRunner.doGet(ServletTestRunner.java:86)
        at jakarta.servlet.api@6.1.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:633)
        at jakarta.servlet.api@6.1.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:723)
        at io.undertow.servlet@2.0.0.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
        at io.undertow.servlet@2.0.0.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
        at io.undertow.servlet@2.0.0.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
        at io.undertow.servlet@2.0.0.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)

Jakarta Dataというより、JPAを使う時にArquillianとどう組み合わせようかという問題な気がしないでもないですが。
テストコードに@Transactionalを付けても効かないようですし、Arquillian Transaction Extensionもだいぶ更新されて
いないようなので困ったのですが。

TCKはどうなっているのだろうと見てみたらUserTransactionをそのまま使っていたので、もうこれでいいかという気に
なりました。

https://github.com/jakartaee/data/blob/1.0.1/tck/src/main/java/ee/jakarta/tck/data/web/transaction/PersistenceTests.java

ポイントはこれくらいですね。

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.3 2026-04-21
OpenJDK Runtime Environment (build 25.0.3+9-2-24.04.2-Ubuntu)
OpenJDK 64-Bit Server VM (build 25.0.3+9-2-24.04.2-Ubuntu, mixed mode, sharing)


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

データベースはMySQLで、IPアドレスは172.17.0.2で動作しているものとします。

 MySQL  localhost:33060+ ssl  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.10    |
+-----------+
1 row in set (0.0008 sec)

準備

Maven依存関係など。

    <properties>
        <maven.compiler.release>25</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>40.0.1.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.14.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.ws.rs</groupId>
            <artifactId>jakarta.ws.rs-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.transaction</groupId>
            <artifactId>jakarta.transaction-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.persistence</groupId>
            <artifactId>jakarta.persistence-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.data</groupId>
            <artifactId>jakarta.data-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-core</artifactId>
            <scope>provided</scope>
        </dependency>

        <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.27.7</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>6.0.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <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>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.15.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.hibernate.orm</groupId>
                            <artifactId>hibernate-processor</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.5.6</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>6.0.0.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>40.0.1.Final</version>
                        <add-ons>
                            <add-on>mysql</add-on>
                        </add-ons>
                        <suggest>true</suggest>
                    </discover-provisioning-info>
                    <env>
                        <MYSQL_DATASOURCE>MySqlDs</MYSQL_DATASOURCE>
                        <MYSQL_JNDI>java:jboss/datasources/MySqlDs</MYSQL_JNDI>
                        <MYSQL_URL>jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&amp;connectionCollation=utf8mb4_0900_bin</MYSQL_URL>
                        <MYSQL_USER>kazuhira</MYSQL_USER>
                        <MYSQL_PASSWORD>password</MYSQL_PASSWORD>
                        <MYSQL_ENABLED>true</MYSQL_ENABLED>
                    </env>
                </configuration>
            </plugin>
        </plugins>
    </build>

前のエントリーから、テストに関する依存関係やプラグインを追加しています。主にはArquillianとREST Assuredですね。

ソースコードも前のエントリーと同じですが、載せておきましょう。

JPAのエンティティークラス。

src/main/java/org/littlewings/jakarta/data/book/Book.java

package org.littlewings.jakarta.data.book;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "book")
public class Book {
    @Id
    private String isbn;

    private String title;

    private Integer price;

    public static Book create(String isbn, String title, Integer price) {
        var book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);
        return book;
    }

    // getter/setterは省略
}

persistence.xml。

src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_2.xsd"
             version="3.2">
    <persistence-unit name="MyPersistenceUnit" transaction-type="JTA">
        <jta-data-source>java:jboss/datasources/MySqlDs</jta-data-source>
        <properties>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>
        </properties>
    </persistence-unit>
</persistence>

Jakarta Dataを使ったリポジトリーインターフェース。

src/main/java/org/littlewings/jakarta/data/book/BookRepository.java

package org.littlewings.jakarta.data.book;

import jakarta.data.Order;
import jakarta.data.Sort;
import jakarta.data.repository.By;
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Find;
import jakarta.data.repository.Param;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;
import java.util.List;

@Repository
public interface BookRepository extends CrudRepository<Book, String> {
    @Find
    Book findByIsbn(String isbn);

    @Find
    List<Book> findAll(Order<Book> order);

    @Find
    Book findByTitle(String title);

    @Find
    Book findByTitle2(@By("title") String name);

    @Query("select b from Book as b where b.price > :greaterThan and b.price < :lessThan")
    List<Book> findByPriceRange(int greaterThan, @Param("lessThan") int p2, Sort<Book> sort);
}

Jakarta RESTful Web Servicesのリソースクラス。

src/main/java/org/littlewings/jakarta/data/book/BookResource.java

package org.littlewings.jakarta.data.book;

import jakarta.data.Order;
import jakarta.data.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
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.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;

@Path("/books")
@ApplicationScoped
@Transactional
public class BookResource {
    @Inject
    private BookRepository bookRepository;

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

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Book> findAll() {
        return bookRepository.findAll(Order.by(Sort.desc(Book_.PRICE)));
    }

    @GET
    @Path("/query")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Book> findByPrice(
            @QueryParam("greaterThanPrice") int greaterThanPrice, @QueryParam("lessThanPrice") int lessThanPrice) {
        return bookRepository.findByPriceRange(greaterThanPrice, lessThanPrice, Sort.asc(Book_.PRICE));
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Book register(Book book) {
        return bookRepository.insert(book);
    }

    @PUT
    @Path("/{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public void update(@PathParam("isbn") String isbn, Book book) {
        bookRepository.findById(isbn).ifPresent(b -> bookRepository.save(book));
    }

    @DELETE
    @Path("/{isbn}")
    public void delete(@PathParam("isbn") String isbn) {
        bookRepository.deleteById(isbn);
    }

    @DELETE
    public void deleteAll() {
        bookRepository.deleteAll(bookRepository.findAll().toList());
    }

    @Inject
    private EntityManager entityManager;

    @POST
    @Path("/rollback")
    @Consumes(MediaType.APPLICATION_JSON)
    public void rollback(List<Book> books) {
        bookRepository.save(books.getFirst());

        entityManager.flush();

        bookRepository.save(books.get(1));

        throw new RuntimeException("Oops!!");
    }
}

JAX-RSの有効化。

src/main/java/org/littlewings/jakarta/data/RestApplication.java

package org.littlewings.jakarta.data;

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

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

テストコードを作成する

それでは、Arquillianを使ったテストコードを作成します。インテーグレーションテストとして書きます。

Arquillianの設定。特になにか設定をしているわけではないので、作成しなくてもよいのですが。

src/test/resources/arquillian.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">
    <container qualifier="remote" default="true">
    </container>
</arquillian>

リポジトリーインターフェースのテストコード

src/test/java/org/littlewings/jakarta/data/book/BookRepositoryIT.java

package org.littlewings.jakarta.data.book;

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

import jakarta.data.Order;
import jakarta.data.Sort;
import jakarta.data.exceptions.EntityExistsException;
import jakarta.data.exceptions.OptimisticLockingFailureException;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.UserTransaction;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.container.annotation.ArquillianTest;
import org.jboss.modules.maven.ArtifactCoordinates;
import org.jboss.modules.maven.MavenResolver;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.Test;
import org.littlewings.jakarta.data.RestApplication;

@ArquillianTest
class BookRepositoryIT {

    @Inject
    private UserTransaction userTransaction;

    @Inject
    private EntityManager entityManager;

    @Inject
    private BookRepository bookRepository;

    @Deployment
    static WebArchive createDeployment() throws IOException {
        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap.create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "")
                .addAsLibraries(MavenResolver.createDefaultResolver()
                        .resolveJarArtifact(ArtifactCoordinates.fromString("org.assertj:assertj-core:3.27.7")));
    }

    private void withTransaction(Runnable runner) {
        try {
            userTransaction.begin();
            entityManager.createNativeQuery("truncate table book").executeUpdate();
            userTransaction.commit();

            userTransaction.begin();

            runner.run();

            userTransaction.commit();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    void basicRepository() {
        withTransaction(() -> {
            var books = List.of(
                    Book.create("978-4774183169", "パーフェクト Java EE", 3520),
                    Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970));

            // insert
            bookRepository.saveAll(books);
            assertThat(bookRepository.findAll().count()).isEqualTo(2);

            assertThat(bookRepository.findById("978-4774183169").get().getTitle())
                    .isEqualTo("パーフェクト Java EE");
            assertThat(bookRepository.findById("978-4297156589").get().getPrice())
                    .isEqualTo(2970);

            // insert
            bookRepository.save(Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960));
            assertThat(bookRepository.findAll().count()).isEqualTo(3);

            // update
            bookRepository.save(Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 3970));
            assertThat(bookRepository.findById("978-4297156589").get().getPrice())
                    .isEqualTo(3970);

            // delete
            bookRepository.deleteById("978-4297136130");
            assertThat(bookRepository.findAll().count()).isEqualTo(2);

            // deleteAll
            bookRepository.deleteAll(books);
            assertThat(bookRepository.findAll().count()).isZero();
        });
    }

    @Test
    void crudRepository() {
        withTransaction(() -> {
            // 存在しないレコードにupdateしても失敗する
            assertThatThrownBy(() -> bookRepository.update(Book.create("978-4774183169", "パーフェクト Java EE", 3520)))
                    .isExactlyInstanceOf(OptimisticLockingFailureException.class)
                    .hasMessage(
                            "Unexpected row count (expected row count 1 but was 0) [update book set price=?,title=? where isbn=?] for entity [org.littlewings.jakarta.data.book.Book with id '978-4774183169']");

            // insert
            bookRepository.insert(Book.create("978-4774183169", "パーフェクト Java EE", 3520));
            assertThat(bookRepository.findAll().count()).isEqualTo(1);
            assertThat(bookRepository.findById("978-4774183169").get().getTitle())
                    .isEqualTo("パーフェクト Java EE");

            // プライマリーキーが重複するレコードをinsertすると失敗する
            assertThatThrownBy(() -> bookRepository.insert(Book.create("978-4774183169", "パーフェクト Java EE", 3520)))
                    .isExactlyInstanceOf(EntityExistsException.class)
                    .hasMessage(
                            "could not execute statement [Duplicate entry '978-4774183169' for key 'book.PRIMARY'] [insert into book (price,title,isbn) values (?,?,?)]");

            // update
            bookRepository.update(Book.create("978-4774183169", "パーフェクト Java EE", 4520));
            assertThat(bookRepository.findAll().count()).isEqualTo(1);
            assertThat(bookRepository.findById("978-4774183169").get().getPrice())
                    .isEqualTo(4520);
        });
    }

    @Test
    void repositoryMethods() {
        withTransaction(() -> {
            var books = List.of(
                    Book.create("978-4774183169", "パーフェクト Java EE", 3520),
                    Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970),
                    Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960));

            bookRepository.insertAll(books);

            // @Find
            assertThat(bookRepository.findByIsbn("978-4774183169").getTitle()).isEqualTo("パーフェクト Java EE");

            // @Find+Order
            var sortedBooks = bookRepository.findAll(Order.by(Sort.desc(Book_.PRICE)));
            assertThat(sortedBooks.getFirst().getTitle()).isEqualTo("プロになるためのSpring入門 -- ゼロからの開発力養成講座");
            assertThat(sortedBooks.getFirst().getPrice()).isEqualTo(3960);
            assertThat(sortedBooks.get(1).getTitle()).isEqualTo("パーフェクト Java EE");
            assertThat(sortedBooks.get(1).getPrice()).isEqualTo(3520);
            assertThat(sortedBooks.getLast().getTitle()).isEqualTo("作って学ぶ Spring Boot入門");
            assertThat(sortedBooks.getLast().getPrice()).isEqualTo(2970);

            // @Find
            assertThat(bookRepository.findByTitle("パーフェクト Java EE").getPrice()).isEqualTo(3520);
            // @Find+@By
            assertThat(bookRepository.findByTitle2("作って学ぶ Spring Boot入門").getPrice())
                    .isEqualTo(2970);

            // @Query+@Param+Sort
            var priceRangeBooks = bookRepository.findByPriceRange(3500, 4000, Sort.asc(Book_.PRICE));
            assertThat(priceRangeBooks).hasSize(2);
            assertThat(priceRangeBooks.getFirst().getTitle()).isEqualTo("パーフェクト Java EE");
            assertThat(priceRangeBooks.get(1).getTitle()).isEqualTo("プロになるためのSpring入門 -- ゼロからの開発力養成講座");
        });
    }
}

今回のポイントは、テストをUserTransactionを使ったトランザクション内で実行していることです。

    private void withTransaction(Runnable runner) {
        try {
            userTransaction.begin();
            entityManager.createNativeQuery("truncate table book").executeUpdate();
            userTransaction.commit();

            userTransaction.begin();

            runner.run();

            userTransaction.commit();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    void basicRepository() {
        withTransaction(() -> {
            var books = List.of(
                    Book.create("978-4774183169", "パーフェクト Java EE", 3520),
                    Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970));

            // insert
            bookRepository.saveAll(books);
            assertThat(bookRepository.findAll().count()).isEqualTo(2);

            assertThat(bookRepository.findById("978-4774183169").get().getTitle())
                    .isEqualTo("パーフェクト Java EE");
            assertThat(bookRepository.findById("978-4297156589").get().getPrice())
                    .isEqualTo(2970);

            // insert
            bookRepository.save(Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960));
            assertThat(bookRepository.findAll().count()).isEqualTo(3);

            // update
            bookRepository.save(Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 3970));
            assertThat(bookRepository.findById("978-4297156589").get().getPrice())
                    .isEqualTo(3970);

            // delete
            bookRepository.deleteById("978-4297136130");
            assertThat(bookRepository.findAll().count()).isEqualTo(2);

            // deleteAll
            bookRepository.deleteAll(books);
            assertThat(bookRepository.findAll().count()).isZero();
        });
    }

それくらいですね…。

JAX-RSリソースクラスのテスト。こちらはクライアント側で、REST Assuredを使ってテストコードを書きます。

src/test/java/org/littlewings/jakarta/data/book/BookResourceIT.java

package org.littlewings.jakarta.data.book;

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

import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef;
import io.restassured.config.ObjectMapperConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.mapper.ObjectMapperType;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit5.container.annotation.ArquillianTest;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.littlewings.jakarta.data.RestApplication;

@ArquillianTest
@RunAsClient
class BookResourceIT {
    @ArquillianResource
    private URL deploymentUrl;

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

    @Deployment
    static WebArchive createDeployment() throws IOException {
        File mainResources = Path.of("src/main/resources").toFile();

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

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = deploymentUrl + resourcePrefix;
        RestAssured.config = RestAssuredConfig.config()
                .objectMapperConfig(
                        ObjectMapperConfig.objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JSONB));

        given().when().delete("/books").then().assertThat().statusCode(Response.Status.NO_CONTENT.getStatusCode());
    }

    @Test
    void registerAndFindSimply() {
        // insert
        var book1 = given().contentType(MediaType.APPLICATION_JSON)
                .body(Book.create("978-4774183169", "パーフェクト Java EE", 3520))
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(Book.class);

        assertThat(book1.getTitle()).isEqualTo("パーフェクト Java EE");

        // find one
        var foundBook1 = given().when()
                .get("/books/978-4774183169")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(Book.class);

        assertThat(foundBook1.getTitle()).isEqualTo("パーフェクト Java EE");

        // insert
        given().contentType(MediaType.APPLICATION_JSON)
                .body(Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970))
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON);

        // insert
        given().contentType(MediaType.APPLICATION_JSON)
                .body(Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960))
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON);

        // findAll
        var allBooks = given().when()
                .get("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(new TypeRef<List<Book>>() {});

        assertThat(allBooks).hasSize(3);
        assertThat(allBooks.getFirst().getTitle()).isEqualTo("プロになるためのSpring入門 -- ゼロからの開発力養成講座");
        assertThat(allBooks.get(1).getTitle()).isEqualTo("パーフェクト Java EE");
        assertThat(allBooks.getLast().getTitle()).isEqualTo("作って学ぶ Spring Boot入門");
    }

    @Test
    void updateAndDelete() {
        var books = List.of(
                Book.create("978-4774183169", "パーフェクト Java EE", 3520),
                Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970),
                Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960));

        books.forEach(book -> given().contentType(MediaType.APPLICATION_JSON)
                .body(book)
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON));

        // update
        given().contentType(MediaType.APPLICATION_JSON)
                .body(Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 4970))
                .when()
                .put("/books/978-4297156589")
                .then()
                .assertThat()
                .statusCode(Response.Status.NO_CONTENT.getStatusCode());

        // find one
        var foundBook = given().when()
                .get("/books/978-4297156589")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(Book.class);

        assertThat(foundBook.getTitle()).isEqualTo("作って学ぶ Spring Boot入門");
        assertThat(foundBook.getPrice()).isEqualTo(4970);

        // delete
        given().when()
                .delete("/books/978-4297136130")
                .then()
                .assertThat()
                .statusCode(Response.Status.NO_CONTENT.getStatusCode());

        // findAll
        var allBooks = given().when()
                .get("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(new TypeRef<List<Book>>() {});

        assertThat(allBooks).hasSize(2);
        assertThat(allBooks.getFirst().getTitle()).isEqualTo("作って学ぶ Spring Boot入門");
        assertThat(allBooks.getLast().getTitle()).isEqualTo("パーフェクト Java EE");
    }

    @Test
    void duplicate() {
        var book1 = given().contentType(MediaType.APPLICATION_JSON)
                .body(Book.create("978-4774183169", "パーフェクト Java EE", 3520))
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(Book.class);

        assertThat(book1.getTitle()).isEqualTo("パーフェクト Java EE");

        // find one
        var foundBook1 = given().when()
                .get("/books/978-4774183169")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(Book.class);

        assertThat(foundBook1.getTitle()).isEqualTo("パーフェクト Java EE");

        given().contentType(MediaType.APPLICATION_JSON)
                .body(Book.create("978-4774183169", "パーフェクト Java EE", 3520))
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
    }

    @Test
    void findByPrice() {
        var books = List.of(
                Book.create("978-4774183169", "パーフェクト Java EE", 3520),
                Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970),
                Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960));

        books.forEach(book -> given().contentType(MediaType.APPLICATION_JSON)
                .body(book)
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON));

        // findAll
        var resultBooks = given().param("greaterThanPrice", 3500)
                .param("lessThanPrice", 4000)
                .when()
                .get("/books/query")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(new TypeRef<List<Book>>() {});

        assertThat(resultBooks).hasSize(2);
        assertThat(resultBooks.getFirst().getTitle()).isEqualTo("パーフェクト Java EE");
        assertThat(resultBooks.getLast().getTitle()).isEqualTo("プロになるためのSpring入門 -- ゼロからの開発力養成講座");
    }

    @Test
    void rollback() {
        var books = List.of(
                Book.create("978-4774183169", "パーフェクト Java EE", 3520),
                Book.create("978-4297156589", "作って学ぶ Spring Boot入門", 2970),
                Book.create("978-4297136130", "プロになるためのSpring入門 -- ゼロからの開発力養成講座", 3960));

        books.forEach(book -> given().contentType(MediaType.APPLICATION_JSON)
                .body(book)
                .when()
                .post("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON));

        var addBooks = List.of(
                Book.create("978-4798180946", "独習Java 第6版", 3278),
                Book.create("978-4295024422", "スッキリわかるJava入門 第5版 (スッキリわかる入門シリーズ)", 3080));

        // rollback test
        given().contentType(MediaType.APPLICATION_JSON)
                .body(addBooks)
                .when()
                .post("/books/rollback")
                .then()
                .assertThat()
                .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());

        // findAll
        var allBooks = given().when()
                .get("/books")
                .then()
                .assertThat()
                .statusCode(Response.Status.OK.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .extract()
                .body()
                .as(new TypeRef<List<Book>>() {});

        assertThat(allBooks).hasSize(3);
    }
}

確認はこんな感じで行います。

WildFlyのプロビジョニング、起動。

$ mvn package -DskipTests -Dwildfly.package.deployment.skip

$ mvn wildfly:start

WildFlyのプロビジョニングをスキップして、テストを実行。

$ mvn verify -Dwildfly.package.skip

テストが終了したら、WildFlyを停止。

$ mvn wildfly:shutdown

おわりに

Jakarta Dataを使ったアプリケーションをArquillianでテストしてみました。

といっても、前回のエントリーを分割しただけなのですが。ただ、Arquillianを使ってテストするのはこれはこれで苦労したので
ちゃんとやっておいてよかったです…。