CDI-Unitを試行してみた
CDI-Unitを試行してみた
📌 3秒まとめ
-
✅ 簡単に CDI のユニットテストができる
-
✅
@Inject
などの基本的な CDI 機能をサポート -
✅ 統合テストは Arquillian、ユニットテストは CDI-Unit という使い分けが可能
-
✅ データベーステストは、DeltaSpike以外はサポートされておらず、工夫が必要
👫 どんな人向けの記事
-
Jakarta EE のアプリケーションに対してJUnitで単体テストを書いている人
-
Arquillian* を使って CDI の自動テストを書いている人
-
これから CDI の自動テストツールを選定する人
* Arquillian は、Java EEおよびJakarta EE向けの自動テストを作成するためのプラットフォームです
🎯 はじめに
Jakarta EE の CDI (Context and Dependency Injection) をテストする際、Arquillian を使っている方も多いと思います。
Arquillian は アプリケーションサーバでの統合テスト に優れていますが、ユニットテストを素早く実行したい場合 には、起動が遅く、セットアップが煩雑になることがあります。
そこで今回は、CDI-Unit というテストライブラリを試してみます。
CDI-Unit を使うと CDI の機能を軽量な環境でテスト でき、単体テストレベルでは、Arquillian の代替手段として有用だと見立てています。
本記事では、CDI-Unitについて簡単な説明と、試行で躓いたデータベースに対するテスト方法を解説します。
CDI-Unitの基本的な使い方は、CDI-Unitのドキュメントを上からなぞればできる為、本記事では解説しません。
CDI-Unit とは?
CDI-Unit は、Jakarta EE の CDI を 軽量な環境でテストするためのライブラリ です。
通常、CDI をテストする場合、WildFly や Payara Micro のようなアプリケーションサーバを起動することが多いです。しかし、CDI-Unit を使えば、JUnit 上で CDI のみを簡単に有効化 し、素早くテストを実行できます。
✅ CDI-Unit のメリット
-
CDI 環境のみを素早く起動できる
-
アプリケーションサーバが不要
-
@Inject
をそのまま利用可能 -
Weld(Jakarta EE における CDI の実装の1つ) との互換性がある
-
JUnit5 に対応
Arquillian との違い
Arquillian を使っている人向けに、CDI-Unit の特徴を整理すると、次のようになります。
項目 | Arquillian | CDI-Unit |
---|---|---|
目的 | 統合テスト(実際のアプリを動かす) | ユニットテスト(CDI のみを対象) |
アプリケーションサーバ | 必要 ※ | 不要(Weld SE で CDI のみ起動) |
テスト速度 | 遅い(アプリケーションサーバの起動が必要) | 高速(JUnit のみで実行) |
CDI のサポート | 広い(ほぼ全ての機能が利用可) | 部分的(@Inject 、@EJB 、@Resource は可) |
モックのサポート | なし | あり |
DB テスト | 可能(JPA + トランザクション管理) | DeltaSpike(JPA + トランザクション管理) |
CDI を 軽量なユニットテストで試したいとき は、CDI-Unit の方が適しています。
実際の環境で動作確認する場合 は、Arquillian が向いています。
※ 厳密には、Arquillianもアプリケーションサーバ不要で CDI テストが可能です。
ただし「目的」の通り、Arquillianは本物の環境を想定したテストツールなので、実態としてサーバ上でテストされることが多いです。また、モックをサポートしていないので、ユニットテストとしての使い方も推奨されないです。
⚠️ CDI-Unitを利用する上で注意点
先述の通り、CDI-Unitでは、CDIのサポートが制限されていることに注意が必要です。
特に、DeltaSpikeを利用していないアプリケーションに対しては、JPA (@PersistenceContext
、@Transactional
) の完全なサポートがないので、例えば以下のようなコードに対しては依存性の注入ができません。
class Sample {
// CDI-Unitでは以下のコードに依存性が注入されない
@PersistenceContext
EntityManager em;
}
これを回避するには、自前の実装が必要です(後述の「🛠️ テスト対象のクラス」を参照)。
実装を工夫することで、トランザクション管理はサポートされないものの、EntityManager
の依存性を解決することができます。
この方法論では、トランザクション管理のテストは他(例:Arquillian)で実施する必要があります。
CDI-Unit のセットアップ
基本的には、公式ガイドと公式のGitHubの内容に従います。
今回は、データベースの事前準備と期待値の検証でDatabase Riderを追加で利用します。
📦 依存関係の追加
Maven プロジェクトで pom.xml
に以下の依存関係を追加します。
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.12.1</version>
<scope>test</scope>
</dependency>
<!-- CDI-Unit -->
<dependency>
<groupId>io.github.cdi-unit</groupId>
<artifactId>cdi-unit</artifactId>
<version>5.0.0-EA7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>5.1.5.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.module</groupId>
<artifactId>weld-web</artifactId>
<version>5.1.5.Final</version>
<scope>test</scope>
</dependency>
<!-- Database Rider -->
<dependency>
<groupId>com.github.database-rider</groupId>
<artifactId>rider-junit5</artifactId>
<version>1.44.0</version>
<classifier>jakarta</classifier>
<scope>test</scope>
</dependency>
<!-- Jakarta EE関連の依存関係 -->
</dependencies>
実際に CDI のテストを書いてみる
CDI-Unit を使って、CDI のテストを動かす方法 を紹介します。
テスト対象クラスは簡単なRepositoryクラスを想定します。
🛠️ テスト対象のクラス
テスト対象は、TaskRepositoryクラスです。
Taskエンティティに対するデータベース操作を行うリポジトリクラスで、主要なCRUD操作を提供しています。
package com.example.task;
import java.util.List;
import java.util.Optional;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import lombok.NoArgsConstructor
@Stateless
@NoArgsConstructor
@Transactional
public class TaskRepository {
private EntityManager em;
@Inject
public TaskRepository(EntityManager em) {
this.em = em;
}
public List<Task> findAll() {
return em.createQuery("SELECT c FROM Task c", Task.class).getResultList();
}
public Optional<Task> findById(int id) {
return Optional.ofNullable(em.find(Task.class, id));
}
public Task create(Task task) {
em.persist(task);
return task;
}
public void delete(int id) {
var task = findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid task Id:" + id));
em.remove(task);
}
public Task update(int id, String title) {
Task ref = em.getReference(Task.class, id);
ref.setTitle(title);
return em.merge(ref);
}
}
💡 テスト対象の周辺クラス
Taskエンティティ
タスクの情報を保持するためのエンティティクラスです。
JPA(Java Persistence API)を使用してデータベースのテーブル(Task)にマッピングされます。
package com.example.task;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column
private String title;
}
Taskテーブル
Taskエンティティに対応するデータベースのテーブルです。
CREATE TABLE IF NOT EXISTS Task (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL
);
データベース設定(persistence.xml)
H2 Database(H2DB)を利用するための設定ファイルです。
具体的には、TaskPersistenceUnit
という名前の永続ユニットを定義し、そのユニットで使用するエンティティクラス(Task)とデータベース接続プロパティを指定しています。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0" 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_0.xsd>
<persistence-unit name="TaskPersistenceUnit">
<class>com.example.task.Task</class>
<properties>
<property name="hibernate.connection.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" />
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"></property>
<property name="hibernate.connection.driver_class" value="org.h2.Driver" />
<property name="hibernate.connection.password" value="admin" />
<property name="hibernate.connection.username" value="admin" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
EntityManager生成クラス
EntityManagerを生成し、CDIコンテナに登録するためのプロデューサークラスです。
persistence.xmlファイルに定義されたTaskPersistenceUnit
という名前の永続ユニットを使用してEntityManagerFactory
を作成し、そのファクトリからEntityManager
を生成します。
package com.example.task;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
@ApplicationScoped
public class EntityManagerProducer {
private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("TaskPersistenceUnit");
@Produces
@RequestScoped
public EntityManager createEntityManager() {
return emf.createEntityManager();
}
}
🧪 CDI-Unit を使ったテスト
TaskRepositoryクラスに対するテストをCDI-Unitで実装します。
今回は代表して、新規作成(create)、更新(update)、削除(delete)の3つのメソッドに対してテストを実装します。
package com.example.task;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import io.github.cdiunit.AdditionalClasses;
import io.github.cdiunit.InRequestScope;
import io.github.cdiunit.junit5.CdiJUnit5Extension;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
@ExtendWith({CdiJUnit5Extension.class})
@AdditionalClasses({EntityManagerProducer.class})
@DBRider
class TaskRepositoryTest {
@Inject
TaskRepository taskRepository;
@Inject
EntityManager entityManager;
@BeforeAll
@DataSet(executeScriptsBefore = "datasets/create_task.sql")
static void initial() {}
@InRequestScope
@Test
@ExpectedDataSet("datasets/expected_tasks_after_insert.yml")
void testCreateTask() {
entityManager.getTransaction().begin();
Task task1 = new Task();
task1.setTitle("task1");
taskRepository.create(task1);
entityManager.getTransaction().commit();
}
@InRequestScope
@Test
@DataSet(value = "datasets/tasks.yml")
@ExpectedDataSet("datasets/expected_tasks_after_update.yml")
void testUpdateTask() {
entityManager.getTransaction().begin();
Task updatedTask = taskRepository.update(1, "updatedTask");
entityManager.getTransaction().commit();
}
@InRequestScope
@Test
@DataSet(value = "datasets/tasks.yml")
@ExpectedDataSet("datasets/expected_tasks_after_delete.yml")
void testDeleteTask() {
entityManager.getTransaction().begin();
assertDoesNotThrow(() -> taskRepository.delete(1));
entityManager.getTransaction().commit();
}
}
💡 テストで利用しているファイル
create_task.sql
TaskテーブルのDDLです。
CREATE TABLE IF NOT EXISTS Task (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL
);
Database Riderはテスト起動時にテーブル作成機能を持っていません。
今回は、H2 Database(H2DB)をインメモリで利用している為、テスト起動時にテーブル作成の必要があり、本DDLを利用します。
expected_tasks_after_insert.yml
Database Riderの主要機能の1つに、テスト実行後のデータベースの状態をファイル(例:yml)と比較して検証する機能があります。
expected_tasks_after_insert.ymlでは、testCreateTask
メソッド実行後のデータベースの状態を定義しています。
Task:
- id: 1
title: "task1"
tasks.yml
Database Riderの他の機能に、テスト実行前にファイル(例:yml)の内容に基づいて、テーブルをセットアップする機能があります。
tasks.ymlでは、testUpdateTask
メソッド、testDeleteTask
メソッド実行前の、Taskテーブルの状態を定義しています。
Task:
- id: 1
title: "task1"
- id: 2
title: "task2"
expected_tasks_after_update.yml
expected_tasks_after_insert.ymlと同様に、testUpdateTask
メソッド実行後のデータベースの状態を定義しています。
Task:
- id: 1
title: "updatedTask" # task1 が更新される
- id: 2
title: "task2"
datasets/expected_tasks_after_delete.yml
expected_tasks_after_insert.ymlと同様に、testDeleteTask
メソッド実行後のデータベースの状態を定義しています。
Task:
- id: 2
title: "task2" # task1 が削除されている
dbunit.yml
Database Riderの設定ファイルです。Database Riderが利用するデータベースの接続情報は、persistence.xmlとは別管理になるため、本ファイルで定義します。
connectionConfig:
driver: "org.h2.Driver"
url: "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
user: "admin"
password: "admin"
テストクラスの解説
@ExtendWith({CdiJUnit5Extension.class})
@AdditionalClasses({EntityManagerProducer.class})
@DBRider
class TaskRepositoryTest {
}
-
@ExtendWith({CdiJUnit5Extension.class})
- Junit5でCDI-Unitを実行するための設定です。
-
@AdditionalClasses({EntityManagerProducer.class})
- CDI-Unitの機能の1つで、テストクラス内の
@Inject
で注入するクラスを明示的に宣言します。
- CDI-Unitの機能の1つで、テストクラス内の
-
@DBRider
- Database Riderを利用するためのアノテーションです。
@Inject
TaskRepository taskRepository;
@Inject
EntityManager entityManager;
-
@Inject TaskRepository taskRepository
- テスト対象クラスです。 CDI-Unitが自動的に依存先を検出して注入します。
-
@Inject EntityManager entityManager
- テスト内で利用する、EntityManagerです。
@AdditionalClasses
で指定したEntityManagerProducer.class
から注入されます。 - DeltaSpikeを利用しない場合、CDI-Unitではトランザクション管理がサポートされません。テストクラスからトランザクション管理するために、利用します。
- テスト内で利用する、EntityManagerです。
@BeforeAll
@DataSet(executeScriptsBefore = "datasets/create_task.sql")
static void initial() {}
- データベースのテストをするために、テスト実行前にTaskテーブルを作成します。
@InRequestScope
@Test
@DataSet(value = "xxxxxx")
@ExpectedDataSet("yyyyyy")
void testZZZZZTask() {
entityManager.getTransaction().begin();
// テストコード
entityManager.getTransaction().commit();
}
-
@InRequestScope
- テストを実行する際のメソッド呼び出しのスコープを定義します。今回はデータベースアクセスなので、リクエストスコープを指定します。
-
@DataSet(value = "xxxxxx")
と@ExpectedDataSet("yyyyyy")
- Database Riderの主要機能で、「テスト実行前」と「テスト実行後」のデータベースの状態をファイルで定義します。
-
entityManager.getTransaction().begin();
とentityManager.getTransaction().commit();
- これまで説明の通り、CDI-Runnerは
@Transactional
をサポートしていない為、明示的にトランザクションの「開始」と「終了」を宣言します。 - この場合、トランザクション管理のテストが実施未済となるため、統合テスト時にArquillianで実施するなど、テスト観点漏れの考慮が必要です。
- これまで説明の通り、CDI-Runnerは
📊 Arquillianで実施するテストとの比較
CDI-UnitとArquillianの主要な違いは前項までに述べた通りですが、テストクラス実装といった観点で見たときは、以下の点でCDI-Unitが優れていると感じました。
-
@Inject
で注入するクラスの指定がほぼ不要- Arquillianは、
@Inject
するクラスなどは全て@Deployment
で明示的に宣言が必要です。パッケージ単位に指定したり、記述を省略する手段はあるものの、かなり手間です。 - 一方で、CDI-Unitは基本的に
@Inject
するクラスの宣言は不要です。 - 本記事の
EntityManagerProducer
クラスなど、一部、自動的に依存先を検出できないものがあり、その場合のみ明示的な指定が必要です。公式ガイド > Controlling the CDI environmentに例が記載されています。
- Arquillianは、
まとめ
CDI-Unit を使うことで、 CDI のユニットテストを素早く実行できます。
また、データベースに対するテストも、一部制約がありますが、実施可能です。
試行した感触では、設定も簡単で快適に利用できました。
アプリケーションサーバ上でのテストではない為、実際の環境と異なる挙動(例:トランザクション管理など)は、別のテストツールでカバーが必要ですが、単体テストツールとしては、候補の1つになると感じました。
なお、掲載したソースコードはサンプルになります。本ソースコードを使用することで発生するいかなる損害や不利益について、当社は一切の責任を負いませんので自己の責任においてご利用ください。
Discussion