このページを編集

Testing components

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 component
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 固有の機能です。

この場合、コンポーネントテストは次のようになります。

Simple component test
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 を注入します。

したがって、上記のコードスニペットは次のように書き直すことができます。

Simple component test with test method parameters
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 アノテーションを使用して、プログラムによってエクステンションを設定できます。

元のテストを次のように書き直すことができます。

Simple component test with programmatic configuration
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 でアノテーションが付けられている場合を除く)。 また、 RepetitionInfoTestInfo など、自動的にスキップされる JUnit 組み込みパラメーターもいくつかあります。

@Inject インジェクションポイントは、CDI Bean のコンテキストインスタンス (テスト対象の実際のコンポーネント) を受け取ります。 @InjectMock インジェクションポイントは、満たされていない依存関係に対して自動的に作成 (unsatisfied dependency automatically を参照) された "未設定" の Mockito モックを受け取ります。

フィールドに注入された依存 Bean とテストメソッド引数は、それぞれテストインスタンスが破棄される前とテストメソッドが完了した後に正しく破棄されます。

Arguments of a @ParameterizedTest method that are provided by an ArgumentsProvider, for example with @org.junit.jupiter.params.provider.ValueArgumentsProvider, must be annotated with @SkipInject.

3.1. テスト対象コンポーネント

テスト対象コンポーネントの初期セットは、テストクラスから派生します。

  1. @jakarta.inject.Inject でアノテーションが付けられたすべてのフィールドの型は、コンポーネント型と見なされます。

  2. @InjectMock@SkipInject、または @org.mockito.Mock でアノテーションが付けられていないテストメソッドパラメーターの型も、コンポーネント型と見なされます。

  3. @QuarkusComponentTest#addNestedClassesAsComponents()true (デフォルト) に設定されている場合、テストクラスで宣言されたすべてのネストされた静的クラスもコンポーネントになります。

@Inject Instance<T> and @Inject @All List<T> injection points are handled specifically. The actual type argument is registered as a component. However, if the type argument is an interface the implementations are not registered automatically.

追加のコンポーネントクラスは、 @QuarkusComponentTest#value() または QuarkusComponentTestExtensionBuilder#addComponentClasses() を使用して設定できます。

3.2. 満たされていない依存関係の自動モック化

通常の CDI 環境とは異なり、コンポーネントが満たされていない依存関係を注入してもテストは失敗しません。 代わりに、満たされていない依存関係に解決されるインジェクションポイントの必要なタイプと修飾子の組み合わせごとに、合成 Bean が自動的に登録されます。 Bean には @Singleton スコープがあるため、Bean は必要な同じタイプと修飾子を持つすべてのインジェクションポイント間で共有されます。 注入される参照は 未設定 の Mockito モックです。 io.quarkus.test.InjectMock アノテーションを使用してテストにモックを注入し、Mockito API を活用して動作を設定できます。

@InjectMock は、Mockito JUnit エクステンションによって提供される機能すべてに代わるものではありません。 これは、CDI Bean の満たされていない依存関係の設定に使用することを目的としたものです。 QuarkusComponentTestMockitoExtension と並行して使用できます。

import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
@QuarkusComponentTest
public class FooTest {

    @TestConfigProperty(key = "bar", value = "true")
    @Test
    public void testPing(Foo foo, @InjectMock Charlie charlieMock, @Mock Ping ping) {
        Mockito.when(ping.pong()).thenReturn("OK");
        Mockito.when(charlieMock.ping()).thenReturn(ping);
        assertEquals("OK", foo.ping());
    }
}

3.3. 満たされていない依存関係のカスタムモック

場合によっては、Bean 属性を完全に制御し、デフォルトのモック動作を設定する必要があることもあります。 QuarkusComponentTestExtensionBuilder#mock() メソッドを介して、モックコンフィギュレーター API を使用できます。

4. ネストしたテスト

JUnit @Nested tests may help to structure more complex test scenarios. However, only basic use cases are tested with @QuarkusComponentTest.

Nested test
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 テストはテスト対象のコンポーネントを挿入します。FooCharlie を挿入します。
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 の場合、このアノテーションはテストクラスでのみ使用でき、テストメソッドでは無視されます。

@io.quarkus.test.component.TestConfigProperty declared on a @Nested test class is always ignored.

また、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();
   }
}

コンポーネントテストは次のように記述できます。

Nested test
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 を注入します。
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 インターセプタークラスは自動的にテスト対象コンポーネントと見なされます。
Static nested classes declared on a test class that is annotated with @QuarkusComponentTest are excluded from bean discovery when running a @QuarkusTest in order to prevent unintentional CDI conflicts.

もう 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 プログラムで宣言されたエクステンションは、テストライフサイクルの後半で常にインスタンス化されるため、この問題を回避できます。

関連コンテンツ