コンポーネントのテスト
Quarkus のコンポーネントモデルは CDI 上に構築されています。
そのため、Quarkus は、コンポーネント/CDI Bean を簡単にテストし、それらの依存関係をモック化できる JUnit エクステンションである QuarkusComponentTestExtension を提供します。
@QuarkusTest とは異なり、このエクステンションは完全な Quarkus アプリケーションを起動せず、CDI コンテナーと設定サービスのみを起動します。
詳細は、ライフサイクル セクションを参照してください。
この JUnit エクステンションは、quarkus-junit-component 依存関係で利用できます。
|
1. 基本例
2 つのインジェクションポイントを持つ CDI Bean であるコンポーネント Foo があるとします。
Foo コンポーネントpackage org.acme;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped (1)
public class Foo {
@Inject
Charlie charlie; (2)
@ConfigProperty(name = "bar")
boolean bar; (3)
public String ping() {
return bar ? charlie.ping() : "nok";
}
}
| 1 | Foo は @ApplicationScoped CDI Bean です。 |
| 2 | Foo は、メソッド ping() を宣言する Charlie に依存します。 |
| 3 | Foo は設定プロパティー bar に依存します。 @Inject は CDI 修飾子も宣言するため、このインジェクションポイントには必要ありません。これは Quarkus 固有の機能です。 |
この場合、コンポーネントテストは次のようになります。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest (1)
@TestConfigProperty(key = "bar", value = "true") (2)
public class FooTest {
@Inject
Foo foo; (3)
@InjectMock
Charlie charlieMock; (4)
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK"); (5)
assertEquals("OK", foo.ping());
}
}
| 1 | QuarkusComponentTest アノテーションは JUnit エクステンションを登録します。 |
| 2 | テストの設定プロパティーを設定します。 |
| 3 | このテストはテスト対象のコンポーネントを注入します。 @Inject でアノテーションが付けられたすべてのフィールドの型が、テスト対象のコンポーネント型とみなされます。 @QuarkusComponentTest#value() を使用して追加のコンポーネントクラスを指定することもできます。さらに、テストクラスで宣言された静的ネストクラスもコンポーネントです。 |
| 4 | このテストは Charlie のモックも注入します。 Charlie は、 @Singleton 合成 Bean が自動的に登録される、満たされていない 依存関係です。注入される参照は、"未設定" の Mockito モックです。 |
| 5 | テストメソッドで Mockito API を活用して動作を設定できます。 |
また、 QuarkusComponentTestExtension は、テストメソッドのパラメーターを解決し、一致する Bean を注入します。
したがって、上記のコードスニペットは次のように書き直すことができます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest
@TestConfigProperty(key = "bar", value = "true")
public class FooTest {
@Test
public void testPing(Foo foo, @InjectMock Charlie charlieMock) { (1)
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
| 1 | @io.quarkus.test.component.SkipInject でアノテーションが付けられたパラメーターは、このエクステンションによって解決されることはありません。 |
さらに、 QuarkusComponentTestExtension 設定を完全に制御する必要がある場合は、 @RegisterExtension アノテーションを使用して、プログラムによってエクステンションを設定できます。
元のテストを次のように書き直すことができます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.QuarkusComponentTestExtension;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class FooTest {
@RegisterExtension (1)
static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder().configProperty("bar","true").build();
@Inject
Foo foo;
@InjectMock
Charlie charlieMock;
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
| 1 | QuarkusComponentTestExtension は、テストクラスの静的フィールドで設定します。 |
2. ライフサイクル
では、 QuarkusComponentTest は実際には何を実行するのでしょうか。
これは、CDI コンテナーを起動し、専用の 設定オブジェクト を登録します。
テストインスタンスのライフサイクルが Lifecycle#PER_METHOD (デフォルト) の場合、コンテナーは before each テストフェーズ中に起動し、 after each テストフェーズ中に停止します。
一方、テストインスタンスのライフサイクルが Lifecycle#PER_CLASS の場合、コンテナーは before all テストフェーズ中に起動し、 after all テストフェーズ中に停止します。
@Inject および @InjectMock でアノテーションが付けられたフィールドは、テストインスタンスが作成された後に注入されます。
一致する Bean が存在するテストメソッドのパラメーターは、テストメソッドの実行時に解決されます (@io.quarkus.test.component.SkipInject または @org.mockito.Mock でアノテーションが付けられている場合を除く)。
最後に、CDI リクエストコンテキストが各テストメソッドごとにアクティブ化され、終了します。
3. インジェクション
@jakarta.inject.Inject および @io.quarkus.test.InjectMock でアノテーションが付けられたテストクラスのフィールドは、テストインスタンスが作成された後に注入されます。
さらに、一致する Bean が存在するテストメソッドのパラメーターが解決されます (@io.quarkus.test.component.SkipInject または @org.mockito.Mock でアノテーションが付けられている場合を除く)。
また、 RepetitionInfo や TestInfo など、自動的にスキップされる JUnit 組み込みパラメーターもいくつかあります。
@Inject インジェクションポイントは、CDI Bean のコンテキストインスタンス (テスト対象の実際のコンポーネント) を受け取ります。
@InjectMock インジェクションポイントは、満たされていない依存関係に対して自動的に作成 (unsatisfied dependency automatically を参照) された "未設定" の Mockito モックを受け取ります。
フィールドに注入された依存 Bean とテストメソッド引数は、それぞれテストインスタンスが破棄される前とテストメソッドが完了した後に正しく破棄されます。
ArgumentsProvider によって提供される @ParameterizedTest メソッドの引数 (たとえば @org.junit.jupiter.params.provider.ValueArgumentsProvider) には、 @SkipInject アノテーションを付ける必要があります。
|
3.1. テスト対象コンポーネント
テスト対象コンポーネントの初期セットは、テストクラスから派生します。
-
@jakarta.inject.Injectでアノテーションが付けられたすべてのフィールドの型は、コンポーネント型と見なされます。 -
@InjectMock、@SkipInject、または@org.mockito.Mockでアノテーションが付けられていないテストメソッドパラメーターの型も、コンポーネント型と見なされます。 -
@QuarkusComponentTest#addNestedClassesAsComponents()がtrue(デフォルト) に設定されている場合、テストクラスで宣言されたすべてのネストされた静的クラスもコンポーネントになります。
@Inject Instance<T> および `@Inject @All List<T>`インジェクションポイントは、特別に処理されます。実際の型引数はコンポーネントとして登録されます。ただし、型引数がインターフェイスの場合、実装は自動的に 登録されません 。
|
追加のコンポーネントクラスは、 @QuarkusComponentTest#value() または QuarkusComponentTestExtensionBuilder#addComponentClasses() を使用して設定できます。
3.2. 満たされていない依存関係の自動モック化
通常の CDI 環境とは異なり、コンポーネントが満たされていない依存関係を注入してもテストは失敗しません。
代わりに、満たされていない依存関係に解決されるインジェクションポイントの必要なタイプと修飾子の組み合わせごとに、合成 Bean が自動的に登録されます。
Bean には @Singleton スコープがあるため、Bean は必要な同じタイプと修飾子を持つすべてのインジェクションポイント間で共有されます。
注入される参照は 未設定 の Mockito モックです。
io.quarkus.test.InjectMock アノテーションを使用してテストにモックを注入し、Mockito API を活用して動作を設定できます。
|
|
4. ネストしたテスト
JUnit @Nested テスト は、より複雑なテストシナリオを構成するのに役立つかもしれません。しかし、@QuarkusComponentTest でテストされるのは基本的なユースケースのみです。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest (1)
@TestConfigProperty(key = "bar", value = "true") (2)
public class FooTest {
@Inject
Foo foo; (3)
@InjectMock
Charlie charlieMock; (4)
@Nested
class PingTest {
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
@Nested
class PongTest {
@Test
public void testPong() {
Mockito.when(charlieMock.pong()).thenReturn("NOK");
assertEquals("NOK", foo.pong());
}
}
}
| 1 | QuarkusComponentTest アノテーションは JUnit エクステンションを登録します。 |
| 2 | テストの設定プロパティーを設定します。 |
| 3 | テストはテスト対象のコンポーネントを挿入します。Foo は Charlie を挿入します。 |
| 4 | このテストは Charlie のモックも注入します。注入された参照は「未設定」のMockitoモックです。 |
5. 設定
@io.quarkus.test.component.TestConfigProperty アノテーションまたは QuarkusComponentTestExtensionBuilder#configProperty(String, String) メソッドを使用して、テストの設定プロパティーを設定できます。
不足している設定プロパティーに対してデフォルト値を使用する必要があるだけの場合は、 @QuarkusComponentTest#useDefaultConfigProperties() または QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties() が役立つ場合があります。
@io.quarkus.test.component.TestConfigProperty アノテーションを使用して、テストメソッドの設定プロパティーを設定することもできます。
ただし、テストインスタンスのライフサイクルが Lifecycle#_PER_CLASS の場合、このアノテーションはテストクラスでのみ使用でき、テストメソッドでは無視されます。
@Nested テスト クラスで宣言された @io.quarkus.test.component.TestConfigProperty は常に無視されます。
|
また、CDI Bean は、注入されたすべての Config Mappings に対して自動的に登録されます。マッピングには、テスト設定プロパティーが入力されます。
5.1. 設定ソース
デフォルトでは、application.properties の設定プロパティーと、@TestConfigProperty アノテーションまたは QuarkusComponentTestExtensionBuilder#configProperty(String, String) メソッドで設定されたプロパティーのみがテスト設定に含まれます。システムプロパティーと環境変数 (ENV variables) は、デフォルトではテスト設定に 含まれません。しかし、@QuarkusComponentTest#useSystemConfigSources() または QuarkusComponentTestExtensionBuilder#useSystemConfigSources() を使用して、この動作を設定できます。
Similarly, discovered config sources such as application.yaml are not included by default.
You can use @QuarkusComponentTest#useDiscoveredConfigSources() or QuarkusComponentTestExtensionBuilder#useDiscoveredConfigSources() to include them.
6. アクティブレコードパターンを使用する Panache エンティティ
アクティブレコードパターンを使用している場合、Mockito#mockStatic() を活用して PanacheEntityBase で宣言されたスタティックメソッドをモックすることは容易ではありません。通常の Quarkus アプリケーションでは、これらのスタティックメソッドはサブクラスで自動的にオーバーライドされます。しかし、QuarkusComponentTest は完全な Quarkus アプリケーションを起動しません。それにもかかわらず、QuarkusComponentTest とシームレスに統合される quarkus-panache-mock モジュールを使用できます。
この依存関係を pom.xml に追加してください。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
このシンプルなエンティティ:
@Entity
public class Person extends PanacheEntity {
public String name;
public Person(String name) {
this.name = name;
}
public static List<Person> findOrdered() {
return find("ORDER BY name").list();
}
}
それはシンプルな Bean で使われています。
public class PersonService {
public List<Person> getPersons(boolen sorted) {
if (sorted) {
return Person.findOrdered();
}
return Person.listAll();
}
}
コンポーネントテストは次のように記述できます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import io.quarkus.panache.mock.MockPanacheEntities;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest (1)
@MockPanacheEntities(Person.class) (2)
public class PersonServiceTest {
@Inject
PersonService personService; (3)
@Test
public void testGetPersons() {
Mockito.when(Person.listAll()).thenReturn(List.of(new Person("Tom")));
List<Person> list = personService.getPersons(false);
assertEquals(1, list.size());
assertEquals("Tom", list.get(0).name);
Mockito.when(Person.findOrdered()).thenReturn(List.of(new Person("Tom")));
list = personService.getPersons(true);
assertEquals(1, list.size());
assertEquals("Tom", list.get(0).name);
}
}
| 1 | QuarkusComponentTest アノテーションは JUnit エクステンションを登録します。 |
| 2 | @MockPanacheEntities は、指定されたエンティティクラスのモックをインストールします。 |
| 3 | このテストは、テスト対象のコンポーネントである PersonService を注入します。 |
| 詳細については、 Panache でシンプルになった Hibernate ORM - アクティブレコードパターンを使用する も参照してください。 |
| 現在、Hibernate ORM との統合のみがサポートされています。MongoDB や Hibernate Reactive など、Panache API のその他のバリアントはまだサポートされていません。 |
7. CDI インターセプターのモック化
テスト対象のコンポーネントクラスがインターセプターバインディングを宣言している場合は、インターセプションもモック化する必要があるかもしれません。 このタスクを達成するには 2 つの方法があります。 まず、テストクラスのネストされた静的クラスとしてインターセプタークラスを定義できます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
public class FooTest {
@Inject
Foo foo;
@Test
public void testPing() {
assertEquals("OK", foo.ping());
}
@ApplicationScoped
static class Foo {
@SimpleBinding (1)
String ping() {
return "ok";
}
}
@SimpleBinding
@Interceptor
static class SimpleInterceptor { (2)
@AroundInvoke
Object aroundInvoke(InvocationContext context) throws Exception {
return context.proceed().toString().toUpperCase();
}
}
}
| 1 | @SimpleBinding はインターセプターバインディングです。 |
| 2 | インターセプタークラスは自動的にテスト対象コンポーネントと見なされます。 |
@QuarkusComponentTest でアノテーションが付けられたテストクラスで宣言された静的ネストクラスは、意図しない CDI 競合を防ぐために、 @QuarkusTest の実行時に Bean 検出から除外されます。
|
もう 1 つの方法は、テストクラスでインターセプターメソッドを直接宣言することです。この場合、メソッドが関連するインターセプションフェーズで呼び出されます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
public class FooTest {
@Inject
Foo foo;
@Test
public void testPing() {
assertEquals("OK", foo.ping());
}
@SimpleBinding (1)
@AroundInvoke (2)
Object aroundInvoke(InvocationContext context) throws Exception {
return context.proceed().toString().toUpperCase();
}
@ApplicationScoped
static class Foo {
@SimpleBinding (1)
String ping() {
return "ok";
}
}
}
| 1 | 結果として得られるインターセプターのインターセプターバインディングは、メソッドにインターセプターバインディングタイプをアノテーション付けすることによって指定されます。 |
| 2 | インターセプトの種類を定義します。 |
8. 他の JUnit エクステンションとの連携テスト
異なる JUnit エクステンションを Quarkus コンポーネントテストと組み合わせて使用することはほとんどの場合問題ありませんが、問題となる場合もあります。
この背景には、Quarkus がクラスローディングを調整する必要があることと、JUnit におけるエクステンションの初期化の性質上、どのエクステンションが最初に起動し、いつ正確にインスタンス化されるかについて限定的な保証しかないという理由があります。
この問題は、@ExtendWith またはそれぞれのエイリアスを使用してクラスレベルで宣言されたエクステンションにのみ発生することに注意してください。さらに、それらはライフサイクルの早い段階でクラスローダーと連携する必要もあります。例えば、コンストラクターで ServiceLoader メカニズムを使用する場合などです。
潜在的な回避策として、@RegisterExtension を介して他のエクステンションをインスタンスフィールドとしてプログラムで登録することが挙げられます。これだけで、そのインスタンス化を遅らせ、問題の発生を防ぐのに十分です。以下に例を示します。
@QuarkusComponentTest
class TestWithMultipleJUnitExtensions {
@RegisterExtension (1)
MyOtherJUnitExtension extension = new MyOtherJUnitExtension();
@Test
void myTest() {
// some testing logic
}
}
| 1 | プログラムで宣言されたエクステンションは、テストライフサイクルの後半で常にインスタンス化されるため、この問題を回避できます。 |