コンポーネントのテスト
Quarkus のコンポーネントモデルは CDI 上に構築されています。
そのため、Quarkus は、コンポーネント/CDI Bean を簡単にテストし、それらの依存関係をモック化できる JUnit エクステンションである QuarkusComponentTestExtension
を提供します。
@QuarkusTest
とは異なり、このエクステンションは完全な Quarkus アプリケーションを起動せず、CDI コンテナーと設定サービスのみを起動します。
詳細は、ライフサイクル セクションを参照してください。
このエクステンションは、 quarkus-junit5-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. 設定
@io.quarkus.test.component.TestConfigProperty
アノテーションまたは QuarkusComponentTestExtensionBuilder#configProperty(String, String)
メソッドを使用して、テストの設定プロパティーを設定できます。
不足している設定プロパティーに対してデフォルト値を使用する必要があるだけの場合は、 @QuarkusComponentTest#useDefaultConfigProperties()
または QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties()
が役立つ場合があります。
@io.quarkus.test.component.TestConfigProperty
アノテーションを使用して、テストメソッドの設定プロパティーを設定することもできます。
ただし、テストインスタンスのライフサイクルが Lifecycle#_PER_CLASS
の場合、このアノテーションはテストクラスでのみ使用でき、テストメソッドでは無視されます。
また、CDI Bean は、注入されたすべての Config Mappings に対して自動的に登録されます。マッピングには、テスト設定プロパティーが入力されます。
5. 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 | インターセプトの種類を定義します。 |