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
環境など
- OS:Lubuntu 20.04 (Windows10 pro/ VirtualBox)
- OpenJDK 11(https://adoptopenjdk.net/)
- Payara Server 5.2021.4
- Jakarta EE8*2
- JUnit(5.7.2)
環境設定
まずは環境設定周りから。
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でJPAのテスト | KATSUMI KOKUZAWA'S BLOG
H2をインメモリで動かすときの注意 - Yamkazu's Blog
DBUnitとH2 Databaseを使ってみた。 - /dev/null
java - How to configure JPA for testing in Maven - Stack Overflow
さいごに
やろうとしていることは そんなに難しいはずはないのに 毎回毎回「ズバリ」が無いのは何故なんだろうと思います。
今回はテストクラス側で Persinstenceを取得するところで思ったようにいかず あれこれ試しました。
とはいえ、ハマるときは かなり長い時間途方にくれることが多いのですが*7、GitHubで他の人の実装を検索するというのをやるようになって視点や観点の切り替えのヒントを得るようにしたら、以前よりは ハマる時間が減ったような気がします。
ということで、最近はブログのコードはGitHubへ上げるようにしています。