システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

JPAをJUnitでテスト

WebAPIであればE2Eテストで十分かもしれませんが、各レイヤーでテストが出来る仕組みがあると後々便利です。
ということで 直近のエントリーで作った JPA(with NativeQuery)を拡張してJUnitでテストをしたいと思います。

はじめに

JPAのテスト実装は 調べると 多少はありました。
ただ Springを使う例はあれど、Java EE/Jakarta EE の例は思ったよりも少なかったです。 普通(?)の使い方はあるのですが、orm/xmlが絡むと極端に少なかったので まとめてみました。 また、EE(JPA)がDBベンダーの差分を抽象化してくれるということで H2をインメモリで使うことで可搬性の高いものにしているつもりです。

なお、今回の実装ですが、Jakarta EE8を使っています*1

リポジトリ

jakarta-ee9-sample/jpa-part-6-junit5 at main · vermeer-1977-blog/jakarta-ee9-sample · GitHub

環境など

環境設定

まずは環境設定周りから。

pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.7.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>jakarta.platform</groupId>
        <artifactId>jakarta.jakartaee-api</artifactId>
        <version>8.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.200</version>
        <!--            
        <scope>runtime</scope>
        <optional>true</optional>
        -->
    </dependency>

    <!-- test -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    
    <dependency>
        <groupId>org.eclipse.persistence</groupId>
        <artifactId>eclipselink</artifactId>
        <version>2.7.7</version>
        <scope>test</scope>
    </dependency>
    <!-- test -->

</dependencies>


<build>
        
    <!-- for unitTest-->
    <testResources>
        <testResource>
            <directory>src/test/resources</directory>
            <filtering>true</filtering>
        </testResource>
    </testResources>
    
    <plugins>
    
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>${maven.compiler.source}</source>
                <target>${maven.compiler.target}</target>
                <encoding>${project.build.sourceEncoding}</encoding>
                <compilerArgument>-proc:none</compilerArgument>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M5</version>
        </plugin>

(... Cargo周りは省略)

</project>

JUnitに関連する依存を足すときには、maven-surefire-pluginも追加します。
H2については、今回 実行するDBとしても使っているのでruntimeを外しました*3

JPA関連としては、JUnit(つまりJava SE)でJPAを実行するための依存org.eclipse.persistenceと、testフォルダの関連資産の読み込みをするためのtestResourcesの追記です。
参考にしたブログでは testResourcesについての言及が無かったので、ひょっとしたら orm.xmlを使わなければ不要な設定なのかもしれません*4

persistence.xml

<persistence-unit name="TEST_PU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    
        <!-- read main class -->
        <jar-file>${project.basedir}/target/classes</jar-file>

        <!-- read orm.xml  -->
        <mapping-file>META-INF/orms/orm.xml</mapping-file>
    
    <properties>
        <property name="javax.persistence.schema-generation.database.action" value="create"/>
        <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
        <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1"/>
        <property name="javax.persistence.jdbc.user" value="sa"/>
        <property name="javax.persistence.jdbc.password" value=""/>
        <property name="eclipselink.logging.level" value="WARNING"/>
        <property name="eclipselink.logging.level.sql" value="FINE"/>
        <property name="eclipselink.logging.level.connection" value="WARNING"/>
    </properties>
</persistence-unit>

transaction-type="RESOURCE_LOCAL"とするのは、JUnitが EEコンテナではなく Java SEで実行されるからです。

ポイントは <jar-file>${project.basedir}/target/classes</jar-file> で mainフォルダでビルドした資産を testフォルダで参照します。
orm.xml は testフォルダでも参照するので、mainと同じパス<mapping-file>META-INF/orms/orm.xml</mapping-file>を指定します。
<jar-file> でパスとしては同じ場所にあるので 何も書かなくても test側でも参照するかと思ったのですが、そういうものではありませんでした。

なお、orm.xmlをMETA-INFの直下に1つだけおいた構成の場合は、<mapping-file>の記述が無くても読み込まれました*5

JUnit

DB起動と接続

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

public class JpaTestExtension implements BeforeAllCallback, AfterAllCallback {

    public static final String PU_NAME = "TEST_PU";

    private static EntityManagerFactory emf;
    public static EntityManager em;

    public static org.h2.tools.Server server;

    @Override
    public void beforeAll(ExtensionContext ec) throws Exception {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            shutdownServer();
        }));

        server = org.h2.tools.Server.createTcpServer().start();
        emf = Persistence.createEntityManagerFactory(PU_NAME);
        em = emf.createEntityManager();
    }

    @Override
    public void afterAll(ExtensionContext ec) throws Exception {
        shutdownServer();
    }

    private static void shutdownServer() {
        if (em != null && em.isOpen()) {
            em.close();
            em = null;
        }

        if (emf != null && emf.isOpen()) {
            emf.close();
            emf = null;
        }
        if (server != null) {
            server.stop();
            server = null;
        }
    }

}

Extensionにして、JPAのテストをしたいケースに付与できるようにしました。 複数のテスト用PUを作った場合は、同様の Extensionを作って対応できます。
PUが1つだけであれば クラスにして継承でも実現できる機能ですが 複数のPUを指定できる @ExtendWithを使うやり方の方が良いと思います。

ちなみにPersistence.createEntityManagerFactory(PU_NAME)のところが なかなか解決できずに苦労しました。

テストコード

@ExtendWith(JpaTestExtension.class)
public class UserDataQueryImplTest {

    @Test
    public void test01() {
        var userData = new UserData(1L);
        userData.setNickName("NickTest");
        userData.setUserName("UserName1 Test2");

        var tx = JpaTestExtension.em.getTransaction();
        tx.begin();
        JpaTestExtension.em.persist(userData);
        tx.commit();

        var query = new UserDataQueryImpl(JpaTestExtension.em);
        var results = query.getUserData();

        assertEquals(1, results.size());
        assertEquals("NickTest", results.get(0).getNickName());
        assertEquals("UserName1", results.get(0).getUserName().getFirstName());
        assertEquals("Test2", results.get(0).getUserName().getLastName());
    }
}

先に作成したExtensionを @@ExtendWithで適用します。
staticなので、利用するときは JpaTestExtension.em のようにして参照します。
なお、PUが1つだけの場合は static importを使うと emだけになるので記述がよりスッキリします。

余談

EntityManagerですが、コンストラクタインジェクションできるようにしています。
Payara ServerのEE9 RCでは出来なかったのですが、EE8を対象にすると出来たので見直しをしています*6。 なお フィールドインジェクションでも、アクセス修飾子をdefault(無印)にしておけば Mockなど使わなくてもインジェクションは出来ます。

該当箇所の抜粋は以下。

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Dependent
public class ResourceProvider {

    @PersistenceContext
    private EntityManager entityManager;

    @Produces
    @Dependent
    public EntityManager getEntityManager() {
        return entityManager;
    }
}
@RequestScoped
public class UserDataQueryImpl implements UserDataQuery {

    private final EntityManager em;

    @Inject
    public UserDataQueryImpl(EntityManager em) {
        this.em = em;
    }

(...)
}

差分はコミットログを

Constructor Injection · vermeer-1977-blog/jakarta-ee9-sample@d6089d5 · GitHub

参考

JUnit 5 ユーザーガイド

JUnitでJPAのテスト | KATSUMI KOKUZAWA'S BLOG

テストケースからのH2起動 - A Memorandum

H2をインメモリで動かすときの注意 - Yamkazu's Blog

DBUnitとH2 Databaseを使ってみた。 - /dev/null

java - How to configure JPA for testing in Maven - Stack Overflow

JUnit5 使い方メモ - Qiita

さいごに

やろうとしていることは そんなに難しいはずはないのに 毎回毎回「ズバリ」が無いのは何故なんだろうと思います。
今回はテストクラス側で Persinstenceを取得するところで思ったようにいかず あれこれ試しました。
とはいえ、ハマるときは かなり長い時間途方にくれることが多いのですが*7GitHubで他の人の実装を検索するというのをやるようになって視点や観点の切り替えのヒントを得るようにしたら、以前よりは ハマる時間が減ったような気がします。
ということで、最近はブログのコードはGitHubへ上げるようにしています。

*1:Jakarta EE9 RCではありません。試したみたのですが うまくいかなかったためです

*2:EE9-RCではありません

*3:text,runtimeという書き方もあるのですがWARNINGが出るので scope自体を外すことにしました

*4:少なくとも 私は testフォルダのpersistance.xmlが読まれなかったため PersistenceUnitが取得出来ませんでした

*5:この辺りの差は良く分かりません。

*6:Payara Serverの公式対応はEE8なので、まだできないこともあるのは仕方ないことです

*7:技術力無いので