コンポーネントのテスト
Quarkus のコンポーネントモデルは CDI 上に構築されています。
そのため、Quarkus は、コンポーネント/CDI Bean を簡単にテストし、それらの依存関係をモック化できる JUnit エクステンションである QuarkusComponentTestExtension を提供します。
@QuarkusTest とは異なり、このエクステンションは完全な Quarkus アプリケーションを起動せず、CDI コンテナーと設定サービスのみを起動します。
詳細は、ライフサイクル セクションを参照してください。
This JUnit extension is available in the quarkus-junit-component dependency.
|
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 tests may help to structure more complex test scenarios.
However, only basic use cases are tested with @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. Config sources
By default, only the config properties from application.properties and properties set by the @TestConfigProperty annotation or with the QuarkusComponentTestExtensionBuilder#configProperty(String, String) method are included in the test config.
System properties and ENV variables are not included in the test config by default.
However, you can use @QuarkusComponentTest#useSystemConfigSources() or QuarkusComponentTestExtensionBuilder#useSystemConfigSources() to configure this behavior.
6. Panache entities using the active record pattern
If you are using the active record pattern you cannot easily leverage Mockito#mockStatic() to mock static methods declared on PanacheEntityBase.
In a normal Quarkus app, these static methods are automatically overridden in subclasses.
However, QuarkusComponentTest does not start a full Quarkus application.
Nevertheless, you can use the quarkus-panache-mock module which integrates seamlessly with QuarkusComponentTest.
この依存関係を 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();
}
}
That is used in a simple bean:
public class PersonService {
public List<Person> getPersons(boolen sorted) {
if (sorted) {
return Person.findOrdered();
}
return Person.listAll();
}
}
You can write your component test like:
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 installs mocks for the given entity classes. |
| 3 | The test injects the component under the test - PersonService. |
| See also Simplified Hibernate ORM with Panache - Using the active record pattern for more information. |
| Currently, only the integration with Hibernate ORM is supported. Other variants of the Panache API, such as MongoDB and Hibernate Reactive, are not supported yet. |
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. Testing alongside other JUnit Extensions
While using different JUnit extensions in combination with Quarkus component testing works fine most of the time, there are cases where it can become problematic.
The reason behind this is that Quarkus needs to juggle with class loading and, due to nature of extension initialization in JUnit, there is limited guarantee as to which extension comes first and when exactly they get instantiated.
Note that this problem should only exhibit for extensions that are declared on the class level using @ExtendWith or their respective aliases. On top of that, they also need to operate with class loader early in their lifecycle - for example using the ServiceLoader mechanism in their constructors.
A potential workaround is to register the other extension programmatically as an instance field via @RegisterExtension. This alone is enough to delay its instantiation and prevent the issue from occurring. Here is an example:
@QuarkusComponentTest
class TestWithMultipleJUnitExtensions {
@RegisterExtension (1)
MyOtherJUnitExtension extension = new MyOtherJUnitExtension();
@Test
void myTest() {
// some testing logic
}
}
| 1 | Programmatically declared extensions are always instantiated later in the test lifecycle, hence circumventing the issue. |