The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

アプリケーションのテスト

Quarkusアプリケーションのテスト方法について説明します。このガイドでは、以下の内容について説明します。

  • JVM モードでのテスト

  • ネイティブモードでのテスト

  • テストへのリソースの注入

1. 前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 17+がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.6

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

  • 入門ガイドに掲載されている、完了済のGreeterアプリケーション

2. アーキテクチャ

このガイドでは、入門ガイドの一部として作成された最初のテストを拡張します。テストへのインジェクションと、ネイティブ実行可能ファイルをテストする方法もカバーしています。

Quarkus は継続テストをサポートしていますが、これについては 継続テストガイド で説明しています。

3. ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitリポジトリをクローンする: git clone https://github.com/quarkusio/quarkus-quickstarts.git 、または アーカイブ をダウンロードします。

The solution is located in the getting-started-testing directory.

このガイドでは、 getting-started ディレクトリーの完成したアプリケーションをすでに持っていることを前提としています。

4. JVM モードでの HTTP ベースのテストの要約

「はじめに」のサンプルから始めた場合は、正しいツールマップの設定を含めて、すでにテストが完了しているはずです。

ビルドファイルには、2 つのテスト依存関係が表示されます。

Maven
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>
Gradle
dependencies {
    testImplementation("io.quarkus:quarkus-junit5")
    testImplementation("io.rest-assured:rest-assured")
}

quarkus-junit5 は、テストフレームワークを制御する @QuarkusTest アノテーションを提供するため、テストには必須です。 rest-assured は必須ではありませんが、HTTP エンドポイントをテストするのに便利な方法です。

JUnit 5を使用しているので、 Surefire Maven Pluginのバージョンを設定する必要があります。デフォルトのバージョンはJUnit 5をサポートしていない為です。

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <configuration>
       <systemPropertyVariables>
          <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
          <maven.home>${maven.home}</maven.home>
       </systemPropertyVariables>
    </configuration>
</plugin>

また、 java.util.logging.manager システムプロパティーを設定して、テストが正しい logmanager と maven.home を使用して、 ${maven.home}/conf/settings.xml からのカスタム設定が適用されるようにしています (存在する場合)。

プロジェクトには簡単なテストも含まれているはずです:

package org.acme.getting.started.testing;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("hello"));
    }

    @Test
    public void testGreetingEndpoint() {
        String uuid = UUID.randomUUID().toString();
        given()
          .pathParam("name", uuid)
          .when().get("/hello/greeting/{name}")
          .then()
            .statusCode(200)
            .body(is("hello " + uuid));
    }

}

このテストはHTTPを使用して、RESTエンドポイントを直接テストします。テストが実行されると、テストが実行される前にアプリケーションが開始されます。

4.1. テストポートの制御

Quarkusはデフォルトではポート 8080 をリッスンしますが、テストを実行する場合はデフォルトで 8081 をリッスンします。これにより、アプリケーションを並行して実行しながらテストを実行することができます。

テストポートの変更

application.propertiesquarkus.http.test-port を設定することで、を HTTP 用にテストで使われるポートを設定出来、 quarkus.http.test-ssl-port を設定することで HTTPS 用にテストで使用するポートを設定することが出来ます。

quarkus.http.test-port=8083
quarkus.http.test-ssl-port=8446

0 を使用すると、(オペレーティングシステムによって割り当てられた)ランダムなポートが使用されることになります。

Quarkusはまた、テストを実行する前にRestAssuredによって使用されるデフォルトのポートを更新するRestAssuredインテグレーションも提供しているため、追加の設定は必要ありません。

4.2. HTTP インタラクションタイムアウトの制御

テストで REST Assured を使用する場合、接続と応答のタイムアウトは 30 秒に設定されます。この設定は quarkus.http.test-timeout プロパティーでオーバーライドできます:

quarkus.http.test-timeout=10s

4.3. URI の挿入

URLをテストに直接注入することも可能で、別のクライアントを使用するのが簡単になります。これは @TestHTTPResource アノテーションで行います。

静的なリソースをロードするための簡単なテストを書いてみましょう。まず、シンプルなHTMLファイルを src/main/resources/META-INF/resources/index.html に作成します:

<html>
    <head>
        <title>Testing Guide</title>
    </head>
    <body>
        Information about testing
    </body>
</html>

これが正しく提供されているかどうかを確認するための簡単なテストを作成します:

package org.acme.getting.started.testing;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class StaticContentTest {

    @TestHTTPResource("index.html") (1)
    URL url;

    @Test
    public void testIndexHtml() throws IOException {
        try (InputStream in = url.openStream()) {
            String contents = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            Assertions.assertTrue(contents.contains("<title>Testing Guide</title>"));
        }
    }
}
1 このアノテーションを使用すると、QuarkusインスタンスのURLを直接注入することができます。アノテーションの値は、URLのパス部分になります。

今のところ @TestHTTPResource では、URL の URI , URL , String 表現を注入することができます。

5. 特定のエンドポイントのテスト

RESTassuredと @TestHTTPResource の両方が、パスをハードコーディングするのではなく、テストするエンドポイントクラスを指定することをサポートしています。現在、Jakarta RESTエンドポイント、Servlet、Reactive Routesの両方をサポートしています。これにより、あるテストがどのエンドポイントをテストしているのかを正確に把握することが非常に容易になります。

これらの例では、以下のようなエンドポイントを想定しています:

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}
現在、Jakarta RESTのコンテキストパスを設定するための @ApplicationPath() アノテーションをサポートしていません。カスタムコンテキストパスが必要な場合は、代わりに quarkus.resteasy.path の設定値を使用してください。

5.1. テストHTTPリソース

io.quarkus.test.common.http.TestHTTPEndpoint アノテーションを使用してエンドポイントのパスを指定することが出来、指定されたエンドポイントからパスが抽出されます。 TestHTTPResource エンドポイントにも値を指定すると、エンドポイントパスの最後に追加されます。

package org.acme.getting.started.testing;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class StaticContentTest {

    @TestHTTPEndpoint(GreetingResource.class)  (1)
    @TestHTTPResource
    URL url;

    @Test
    public void testIndexHtml() throws IOException {
        try (InputStream in = url.openStream()) {
            String contents = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            Assertions.assertEquals("hello", contents);
        }
    }
}
1 GreetingResource@Path("/hello") とアノテーションされているので、注入された URL は /hello で終わります。

5.2. RESTassured

RESTassured ベースパス (すなわち、すべてのリクエストのルートとなるデフォルトパス) を制御するには、 io.quarkus.test.common.http.TestHTTPEndpoint アノテーションを使用できます。これはクラスやメソッドレベルで適用できます。グリーティングリソースをテストするには、以下のようにします:

package org.acme.getting.started.testing;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static io.restassured.RestAssured.when;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
@TestHTTPEndpoint(GreetingResource.class) (1)
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        when().get()    (2)
          .then()
             .statusCode(200)
             .body(is("hello"));
    }
}
1 これにより、RESTAssured はすべてのリクエストの前に /hello を付けます。
2 このテストでは /hello がデフォルトなので、ここでパスを指定する必要はないことに注意してください。

6. テストへの注入

これまでは、HTTP エンドポイントを介してアプリをテストする統合スタイルのテストしか取り上げてきませんでしたが、ユニットテストを行い、Beanを直接テストしたい場合はどうでしょうか?

Quarkusでは、 @Inject アノテーションを介してテストにCDI Beanを注入できるようにすることで、これをサポートしています(実際、Quarkusのテストは完全なCDI Beanなので、すべてのCDI機能を使用することができます)。HTTPを使用せずにグリーティングサービスを直接テストするシンプルなテストを作成してみましょう。

package org.acme.getting.started.testing;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class GreetingServiceTest {

    @Inject (1)
    GreetingService service;

    @Test
    public void testGreetingService() {
        Assertions.assertEquals("hello Quarkus", service.greeting("Quarkus"));
    }
}
1 GreetingService Beanがテストに注入されます。

7. テストへのインターセプターの適用

前述したように、Quarkusのテストは実際には完全なCDI Beanであり、通常のようにCDIインターセプターを適用することができます。例えば、トランザクションのコンテキスト内でテストメソッドを実行したい場合、 @Transactional アノテーションをメソッドに適用するだけで、トランザクションインターセプターがそれを処理します。

これに加えて、独自のテストステレオタイプを作成することもできます。例えば、以下のように @TransactionalQuarkusTest を作成することができます。

@QuarkusTest
@Stereotype
@Transactional
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TransactionalQuarkusTest {
}

このアノテーションをテストクラスに適用すると、 @QuarkusTest@Transactional の両方のアノテーションを適用したかのように動作します。

@TransactionalQuarkusTest
public class TestStereotypeTestCase {

    @Inject
    UserTransaction userTransaction;

    @Test
    public void testUserTransaction() throws Exception {
        Assertions.assertEquals(Status.STATUS_ACTIVE, userTransaction.getStatus());
    }

}

8. テストとトランザクション

テストでは標準のQuarkus @Transactional アノテーションを使用することができますが、これは、テストでデータベースに加えた変更が永続化されることを意味します。テストの終了時に変更をロールバックしたい場合は、 io.quarkus.test.TestTransaction アノテーションを使用することができます。これは、トランザクション内でテストメソッドを実行しますが、テストメソッドが完了したらロールバックして、データベースの変更を元に戻します。

9. QuarkusTest* コールバックによる強化

インターセプターの代わりに、あるいはインターセプターに加えて、以下のコールバックインターフェースを実装することで、 すべての @QuarkusTest クラスを充実させることができます。

  • io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback

  • io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback

  • io.quarkus.test.junit.callback.QuarkusTestBeforeTestExecutionCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterTestExecutionCallback

  • io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback

オプションとして、プロパティ quarkus.test.enable-callbacks-for-integration-teststrue とすることで、これらのコールバックを @QuarkusIntegrationTest テストでも有効にすることができます。

このようなコールバックの実装は、 java.util.ServiceLoader で定義されている「サービスプロバイダ」として登録する必要があります。

例えば、以下のようなサンプルコールバックです:

package org.acme.getting.started.testing;

import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;

public class MyQuarkusTestBeforeEachCallback implements QuarkusTestBeforeEachCallback {

    @Override
    public void beforeEach(QuarkusTestMethodContext context) {
        System.out.println("Executing " + context.getTestMethod());
    }
}

次のように、src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback を介して登録する必要があります。

org.acme.getting.started.testing.MyQuarkusTestBeforeEachCallback
テストクラスやメソッドからアノテーションを読み込んで、コールバックが何をするかを制御することができます。
While it is possible to use JUnit Jupiter callback interfaces like BeforeEachCallback, you might run into classloading issues because Quarkus has to run tests in a custom classloader which JUnit is not aware of.

10. 異なるプロファイルのテスト

これまでの例では、すべてのテストで Quarkus を 1 回だけ起動しました。最初のテストが実行される前に Quarkus が起動し、その後すべてのテストが実行され、最後に Quarkus がシャットダウンされます。これにより、非常に高速なテストが可能になりますが、様々な設定をテストすることができないため、少し制限されます。

この問題を回避するために、Quarkusはテストプロファイルの考え方をサポートしています。以前に実行したテストとは異なるプロファイルを持つテストがある場合、Quarkusはテストを実行する前にシャットダウンされ、新しいプロファイルで開始されます。これは、テスト時間にシャットダウン/起動サイクルが追加されるため、明らかに少し遅くなりますが、非常に大きな柔軟性が得られます。

Quarkus の再起動回数を減らすために、JUnit 5 User Guide にあるように io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer がグローバルな ClassOrderer として登録されています。この ClassOrderer の動作は junit-platform.properties で設定することができます (詳細はソースコードや javadoc を参照してください)。また、JUnit 5 が提供する別の ClassOrderer を設定することで、完全に無効にすることも可能です (独自のものを設定することも可能です)。さらに JUnit 5.8.2 の時点は、単一の junit-platform.properties のみがピックアップされ、複数見つかった場合は警告が記録されることに注意してください。 このような警告が発生した場合は、Quarkus が提供する junit-platform.properties をクラスパスから除外することで、警告を取り除くことができます。

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5-properties</artifactId>
        </exclusion>
    </exclusions>
</dependency>

10.1. プロフィールの書き方

テストプロファイルを実装するには、 io.quarkus.test.junit.QuarkusTestProfile .

package org.acme.getting.started.testing;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.enterprise.inject.Produces;

import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;

public class MockGreetingProfile implements QuarkusTestProfile { (1)

    /**
     * Returns additional config to be applied to the test. This
     * will override any existing config (including in application.properties),
     * however existing config will be merged with this (i.e. application.properties
     * config will still take effect, unless a specific config key has been overridden).
     *
     * Here we are changing the Jakarta REST root path.
     */
    @Override
    public Map<String, String> getConfigOverrides() {
        return Collections.singletonMap("quarkus.resteasy.path","/api");
    }

    /**
     * Returns enabled alternatives.
     *
     * This has the same effect as setting the 'quarkus.arc.selected-alternatives' config key,
     * however it may be more convenient.
     */
    @Override
    public Set<Class<?>> getEnabledAlternatives() {
        return Collections.singleton(MockGreetingService.class);
    }

    /**
     * Allows the default config profile to be overridden. This basically just sets the quarkus.test.profile system
     * property before the test is run.
     *
     * Here we are setting the profile to test-mocked
     */
    @Override
    public String getConfigProfile() {
        return "test-mocked";
    }

    /**
     * Additional {@link QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this
     * specific test profile.
     *
     * If this method is not overridden, then only the {@link QuarkusTestResourceLifecycleManager} classes enabled via the {@link io.quarkus.test.common.QuarkusTestResource} class
     * annotation will be used for the tests using this profile (which is the same behavior as tests that don't use a profile at all).
     */
    @Override
    public List<TestResourceEntry> testResources() {
        return Collections.singletonList(new TestResourceEntry(CustomWireMockServerManager.class));
    }


    /**
     * If this returns true then only the test resources returned from {@link #testResources()} will be started,
     * global annotated test resources will be ignored.
     */
    @Override
    public boolean disableGlobalTestResources() {
        return false;
    }

    /**
     * The tags this profile is associated with.
     * When the {@code quarkus.test.profile.tags} System property is set (its value is a comma separated list of strings)
     * then Quarkus will only execute tests that are annotated with a {@code @TestProfile} that has at least one of the
     * supplied (via the aforementioned system property) tags.
     */
    @Override
    public Set<String> tags() {
        return Collections.emptySet();
    }

    /**
     * The command line parameters that are passed to the main method on startup.
     */
    @Override
    public String[] commandLineParameters() {
        return new String[0];
    }

    /**
     * If the main method should be run.
     */
    @Override
    public boolean runMainMethod() {
        return false;
    }

    /**
     * If this method returns true then all {@code StartupEvent} and {@code ShutdownEvent} observers declared on application
     * beans should be disabled.
     */
    @Override
    public boolean disableApplicationLifecycleObservers() {
        return false;
    }

    @Produces (2)
    public ExternalService mockExternalService() {
       return new ExternalService("mock");
    }
}
1 これらのメソッドにはすべてデフォルトの実装があるため、オーバーライドが必要なメソッドのみオーバーライドします。
2 If a test profile implementation declares a CDI bean (via producer method/field or nested static class) then this bean is only taken into account if the test profile is used, i.e. it’s ignored for any other test profile.

これでプロファイルを定義したので、それをテストクラスに含める必要があります。そのためには、テストクラスに @TestProfile(MockGreetingProfile.class) アノテーションを付けます。

テストプロファイルの設定はすべて単一のクラスに保存されているので、前回のテストが同じ設定で実行されたかどうかが簡単にわかります。

10.2. 特定のテストの実行

Quarkus は、テストの実行を特定の @TestProfile アノテーションを持つテストに制限できます。これは、QuarkusTestProfiletags メソッドを quarkus.test.profile.tags システムプロパティーと組み合わせて利用することで機能します。

基本的に、quarkus.test.profile.tags の値と一致するタグが少なくとも 1 つある QuarkusTestProfile はアクティブであると見なされ、アクティブなプロファイルの @TestProfile でアノテーションが付けられたすべてのテストが実行されますが、それ以外はスキップされます。以下は、その典型的な例です。

まず、次のようないくつかの QuarkusTestProfile 実装を定義しましょう。

public class Profiles {

    public static class NoTags implements QuarkusTestProfile {

    }

    public static class SingleTag implements QuarkusTestProfile {
        @Override
        public Set<String> tags() {
            return Set.of("test1");
        }
    }

    public static class MultipleTags implements QuarkusTestProfile {
        @Override
        public Set<String> tags() {
            return Set.of("test1", "test2");
        }
    }
}

ここで、次のテストがあると仮定します。

@QuarkusTest
public class NoQuarkusProfileTest {

    @Test
    public void test() {
        // test something
    }
}
@QuarkusTest
@TestProfile(Profiles.NoTags.class)
public class NoTagsTest {

    @Test
    public void test() {
        // test something
    }
}
@QuarkusTest
@TestProfile(Profiles.SingleTag.class)
public class SingleTagTest {

    @Test
    public void test() {
        // test something
    }
}
@QuarkusTest
@TestProfile(Profiles.MultipleTags.class)
public class MultipleTagsTest {

    @Test
    public void test() {
        // test something
    }
}

次のシナリオを考えてみましょう。

  • quarkus.test.profile.tags が設定されていない: すべてのテストが実行されます。

  • quarkus.test.profile.tags=foo: この場合、QuarkusTestProfile 実装で定義されたタグはいずれも quarkus.test.profile.tags の値と一致しないため、テストは実行されません。@TestProfile でアノテーションが付けられていないため、NoQuarkusProfileTest も実行されないことに注意してください。

  • quarkus.test.profile.tags=test1: この場合、それぞれの QuarkusTestProfile 実装のタグが quarkus.test.profile.tags の値と一致するため、SingleTagTestMultipleTagsTest が実行されます。

  • quarkus.test.profile.tags=test1,test3: この場合、前の場合と同じテストが実行されます。

  • quarkus.test.profile.tags=test2,test3: この場合、MultipleTagsTest は、tags メソッドが quarkus.test.profile.tags の値と一致する唯一の QuarkusTestProfile 実装であるため、MultipleTagsTest のみ実行されます。

11. モックサポート

Quarkusでは、2つの異なるアプローチを使用したモックオブジェクトの使用をサポートしています。CDIの代替品を使用してすべてのテストクラスのBeanをモックアウトするか、 QuarkusMock を使用してテストごとにBeanをモックアウトすることができます。

11.1. CDI @Alternative メカニズム

これを使用するには、 src/test/java ディレクトリーのクラスでモックしたいBeanをオーバーライドし、 @Alternative@Priority(1) アノテーションをBeanに配置するだけです。あるいは、便利な io.quarkus.test.Mock ステレオタイプアノテーションを使用することもできます。この組み込みステレオタイプは、 @Alternative@Priority(1)@Dependent を宣言します。例えば、以下のようなサービスがあるとします:

@ApplicationScoped
public class ExternalService {

    public String service() {
        return "external";
    }

}

src/test/java で以下のクラスでモックできました:

@Mock
@ApplicationScoped (1)
public class MockExternalService extends ExternalService {

    @Override
    public String service() {
        return "mock";
    }
}
1 @Mock ステレオタイプで宣言された @Dependent スコープをオーバーライドします。

代替品が src/main/java ではなく src/test/java ディレクトリーに存在することが重要です。そうでなければ、テスト以外も常に有効になってしまいます。

現時点では、このアプローチはネイティブイメージテストでは機能しないことに注意してください。これには、代替テストをネイティブイメージに焼き付ける必要があるためです。

11.2. QuarkusMock を使用したモッキング

io.quarkus.test.junit.QuarkusMock クラスは、通常のスコープ付きBeanを一時的にモックアウトするために使用することができます。 @BeforeAll メソッドでこのメソッドを使用した場合、モックは現在のクラスのすべてのテストに対して有効になりますが、test メソッドでこれを使用した場合、モックは現在のテストの間のみ有効になります。

この方法は、通常のスコープ付き CDI Bean(例: @ApplicationScoped , @RequestScoped など、 @Singleton@Dependent 以外の基本的にすべてのスコープ)に対して使用することができます。

使用例は次のようになります:

@QuarkusTest
public class MockTestCase {

    @Inject
    MockableBean1 mockableBean1;

    @Inject
    MockableBean2 mockableBean2;

    @BeforeAll
    public static void setup() {
        MockableBean1 mock = Mockito.mock(MockableBean1.class);
        Mockito.when(mock.greet("Stuart")).thenReturn("A mock for Stuart");
        QuarkusMock.installMockForType(mock, MockableBean1.class);  (1)
    }

    @Test
    public void testBeforeAll() {
        Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
        Assertions.assertEquals("Hello Stuart", mockableBean2.greet("Stuart"));
    }

    @Test
    public void testPerTestMock() {
        QuarkusMock.installMockForInstance(new BonjourGreeter(), mockableBean2); (2)
        Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
        Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart"));
    }

    @ApplicationScoped
    public static class MockableBean1 {

        public String greet(String name) {
            return "Hello " + name;
        }
    }

    @ApplicationScoped
    public static class MockableBean2 {

        public String greet(String name) {
            return "Hello " + name;
        }
    }

    public static class BonjourGreeter extends MockableBean2 {
        @Override
        public String greet(String name) {
            return "Bonjour " + name;
        }
    }
}
1 インジェクションされたインスタンスはここでは利用できないので、 installMockForType を使用します。このモックは両方のテストメソッドに使用されます。
2 私たちは installMockForInstance を使用して注入されたBeanを置き換えます。

Mockitoには依存しないことに注意してください。好きなモッキングライブラリを使うことができますし、必要な動作を提供するためにオブジェクトを手動でオーバーライドすることもできます。

Using @Inject will get you a CDI proxy to the mock instance you install, which is not suitable for passing to methods such as Mockito.verify which want the mock instance itself. So if you need to call methods such as verify you should hang on to the mock instance in your test, or use @io.quarkus.test.InjectMock.

11.2.1. @InjectMock での更なる単純化

Building on the features provided by QuarkusMock, Quarkus also allows users to effortlessly take advantage of Mockito for mocking the beans supported by QuarkusMock.

This functionality is available with the @io.quarkus.test.InjectMock annotation only if the quarkus-junit5-mockito dependency is present:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>

@InjectMock を使用すると、先ほどの例は次のように書くことができます:

@QuarkusTest
public class MockTestCase {

    @InjectMock
    MockableBean1 mockableBean1; (1)

    @InjectMock
    MockableBean2 mockableBean2;

    @BeforeEach
    public void setup() {
        Mockito.when(mockableBean1.greet("Stuart")).thenReturn("A mock for Stuart"); (2)
    }

    @Test
    public void firstTest() {
        Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
        Assertions.assertEquals(null, mockableBean2.greet("Stuart")); (3)
    }

    @Test
    public void secondTest() {
        Mockito.when(mockableBean2.greet("Stuart")).thenReturn("Bonjour Stuart"); (4)
        Assertions.assertEquals("A mock for Stuart", mockableBean1.greet("Stuart"));
        Assertions.assertEquals("Bonjour Stuart", mockableBean2.greet("Stuart"));
    }

    @ApplicationScoped
    public static class MockableBean1 {

        public String greet(String name) {
            return "Hello " + name;
        }
    }

    @ApplicationScoped
    public static class MockableBean2 {

        public String greet(String name) {
            return "Hello " + name;
        }
    }
}
1 @InjectMock を使用すると、Mockitoのモックが作成され、テストクラスのテストメソッドで利用できるようになります(他のテストクラスはこの影響を 受けません )。
2 クラスのすべてのテストメソッドに対して mockableBean1 が設定されています。
3 mockableBean2 のモックが設定されていないので、デフォルトの Mockito レスポンスを返します。
4 このテストでは、 mockableBean2 が設定されているので、設定されたレスポンスを返します。

上のテストは @InjectMock の機能を示すのには良いですが、実際のテストを上手く表してはいません。実際のテストでは、ほとんどの場合、モックを設定し、モックされたBeanを使用するBeanをテストします。以下に例を示します:

@QuarkusTest
public class MockGreetingServiceTest {

    @InjectMock
    GreetingService greetingService;

    @Test
    public void testGreeting() {
        when(greetingService.greet()).thenReturn("hi");
        given()
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hi")); (1)
    }

    @Path("greeting")
    public static class GreetingResource {

        final GreetingService greetingService;

        public GreetingResource(GreetingService greetingService) {
            this.greetingService = greetingService;
        }

        @GET
        @Produces("text/plain")
        public String greet() {
            return greetingService.greet();
        }
    }

    @ApplicationScoped
    public static class GreetingService {
        public String greet(){
            return "hello";
        }
    }
}
1 greetingService をモックとして設定したので、 GreetingService Beanを使用する GreetingResource は、通常の GreetingService Beanのレスポンスの代わりにモックされたレスポンスを取得します。

By default, the @InjectMock annotation can be used for any normal CDI scoped bean (e.g. @ApplicationScoped, @RequestScoped). Mocking @Singleton beans can be performed by adding the @MockitoConfig(convertScopes = true) annotation. This will convert the @Singleton bean to an @ApplicationScoped bean for the test.

これは高度なオプションと見なされ、Bean のスコープを変更することで引き起こされる結果を完全に理解している場合にのみ実行する必要があります。

11.2.2. @InjectSpy で、モックの代わりにスパイを使用する

InjectMock で提供されている機能をベースに、 QuarkusMock でサポートされているBeanをスパイするために Mockito を簡単に利用できるようにしました。この機能は、 quarkus-junit5-mockito 依存関係で利用可能な @io.quarkus.test.junit.mockito.InjectSpy アノテーションを介して利用できます。

テストの際に、特定の論理パスが通ったことを確認するだけでよい場合や、Spyされたクローン上の残りのメソッドを実行したまま、単一のメソッドのレスポンスをスタブアウトするだけでよい場合があります。Spy部分モックの詳細については、 Mockitoドキュメント - Spying on real objects を参照してください。いずれの場合も、オブジェクトのSpyを使用することが望ましいです。 @InjectSpy を使うと、先ほどの例は次のように書くことができます:

@QuarkusTest
public class SpyGreetingServiceTest {

    @InjectSpy
    GreetingService greetingService;

    @Test
    public void testDefaultGreeting() {
        given()
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hello"));

        Mockito.verify(greetingService, Mockito.times(1)).greet(); (1)
    }

    @Test
    public void testOverrideGreeting() {
        doReturn("hi").when(greetingService).greet(); (2)
        given()
                .when().get("/greeting")
                .then()
                .statusCode(200)
                .body(is("hi")); (3)
    }

    @Path("greeting")
    public static class GreetingResource {

        final GreetingService greetingService;

        public GreetingResource(GreetingService greetingService) {
            this.greetingService = greetingService;
        }

        @GET
        @Produces("text/plain")
        public String greet() {
            return greetingService.greet();
        }
    }

    @ApplicationScoped
    public static class GreetingService {
        public String greet(){
            return "hello";
        }
    }
}
1 値をオーバーライドするのではなく、 GreetingService の greet メソッドがこのテストで呼び出されたことを確認したいだけです。
2 ここでは、Spyに "hello "の代わりに "hi "を返すように指示しています。 GreetingResourceGreetingService に挨拶を要求すると、通常の GreetingService Bean の応答の代わりに、モックされた応答が返ってきます。スパイをスタブするために when(Object) を使用することが不可能であったり、現実的でなかったりすることがあります。したがって、spyを使用する場合は、スタブ用のメソッドである doReturn|Answer|Throw() ファミリーを検討してください。
3 私たちは、スパイからのモックされた応答を得ることを検証しています。

11.2.3. @InjectMock との併用 @RestClient

The @RegisterRestClient registers the implementation of the REST Client at runtime, and because the bean needs to be a regular scope, you have to annotate your interface with @ApplicationScoped.

@Path("/")
@ApplicationScoped
@RegisterRestClient
public interface GreetingService {

    @GET
    @Path("/hello")
    @Produces(MediaType.TEXT_PLAIN)
    String hello();
}

テストクラスの例です:

@QuarkusTest
public class GreetingResourceTest {

    @InjectMock
    @RestClient (1)
    GreetingService greetingService;

    @Test
    public void testHelloEndpoint() {
        Mockito.when(greetingService.hello()).thenReturn("hello from mockito");

        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("hello from mockito"));
    }

}
1 この注入ポイントが RestClient のインスタンスを使用することを意味していることを示します。

11.3. Mocking with Panache

quarkus-hibernate-orm-panachequarkus-mongodb-panache のエクステンションを使っている場合は、Hibernate ORM と Panache モック および MongoDB と Panache モック のドキュメントをチェックして、データアクセスをモックする最も簡単な方法を確認してください。

12. セキュリティーのテスト

Quarkus Securityを使用している場合、アプリケーションのセキュリティー機能を簡単にテストする方法については、セキュリティーのテスト のセクションをご覧ください。

13. Quarkus アプリケーションの開始前にサービスを開始する

非常に一般的なニーズは、Quarkusアプリケーションがテストを開始する前に、Quarkusアプリケーションに依存するいくつかのサービスを開始することです。このニーズに対応するために、Quarkusでは、 @io.quarkus.test.common.QuarkusTestResourceio.quarkus.test.common.QuarkusTestResourceLifecycleManager を提供します。

テストスイート内のテストに @QuarkusTestResource のアノテーションを付けるだけで、Quarkus は、テストが実行される前に対応する QuarkusTestResourceLifecycleManager を実行します。テストスイートは、複数の @QuarkusTestResource アノテーションを自由に利用することもできます。この場合、対応するすべての QuarkusTestResourceLifecycleManager オブジェクトがテストの前に実行されます。複数のテストリソースを使用する場合、それらを同時に開始できます。そのためには、@QuarkusTestResource(parallel = true) を設定する必要があります。

テストリソースは、テストクラスまたはカスタムプロファイルで定義されている場合でもグローバルです。つまり、重複を削除しても、すべてのテストですべてアクティブになります。単一のテストクラスまたはテストプロファイルに限定してテストリソースを有効にする場合は、@QuarkusTestResource(restrictToAnnotatedClass = true) を使用できます。

Quarkusでは、QuarkusTestResourceLifecycleManager の実装がいくつか提供されていますが (H2 データベースを起動する io.quarkus.test.h2.H2DatabaseTestResource や、モック Kubernetes API サーバーを起動する io.quarkus.test.kubernetes.client.KubernetesServerTestResource を参照)、特定のアプリケーションのニーズに対応するためにカスタム実装を作成するのが一般的です。一般的なケースとしては、Testcontainers を使用した docker コンテナーの起動 (その例は こちら を参照)、Wiremock を使用したモック HTTP サーバの起動 (その例は こちら を参照) などがあります。

As QuarkusTestResourceLifecycleManager is not a CDI Bean, classes that implement it can’t have fields injected with @Inject. You can use String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class);

13.1. テストクラスの変更

When creating a custom QuarkusTestResourceLifecycleManager that needs to inject something into the test class, the inject methods can be used. If for example you have a test like the following:

@QuarkusTest
@QuarkusTestResource(MyWireMockResource.class)
public class MyTest {

    @InjectWireMock // this a custom annotation you are defining in your own application
    WireMockServer wireMockServer;

    @Test
    public someTest() {
        // control wiremock in some way and perform test
    }
}

次のコードスニペットの inject メソッドに示すように、MyWireMockResourcewireMockServer フィールドを注入させることができます:

public class MyWireMockResource implements QuarkusTestResourceLifecycleManager {

    WireMockServer wireMockServer;

    @Override
    public Map<String, String> start() {
        wireMockServer = new WireMockServer(8090);
        wireMockServer.start();

        // create some stubs

        return Map.of("some.service.url", "localhost:" + wireMockServer.port());
    }

    @Override
    public synchronized void stop() {
        if (wireMockServer != null) {
            wireMockServer.stop();
            wireMockServer = null;
        }
    }

    @Override
    public void inject(TestInjector testInjector) {
        testInjector.injectIntoFields(wireMockServer, new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class));
    }
}
テストクラスへのこの注入は CDI の制御下になく、CDI がテストクラスへの必要な注入を実行した後に発生することは言及に値します。

13.2. アノテーションベースのテストリソース

アノテーションを使用して有効化および設定されたテストリソースを作成することができます。これは、テストリソースの有効化と設定に使用されるアノテーション に @QuarkusTestResource を配置することで可能になります。

たとえば、これは @WithKubernetesTestServer アノテーションを定義します。これは、テストで KubernetesServerTestResource をアクティブ化するために使用できますが、アノテーションが付けられたテストクラスに限定されます。これは QuarkusTestProfile テストプロファイルに配置することもできます。

@QuarkusTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WithKubernetesTestServer {
    /**
     * Start it with HTTPS
     */
    boolean https() default false;

    /**
     * Start it in CRUD mode
     */
    boolean crud() default true;

    /**
     * Port to use, defaults to any available port
     */
    int port() default 0;
}

KubernetesServerTestResource クラスは、前のアノテーションを使用して設定するために、QuarkusTestResourceConfigurableLifecycleManager インターフェイスを実装する必要があります。

public class KubernetesServerTestResource
        implements QuarkusTestResourceConfigurableLifecycleManager<WithKubernetesTestServer> {

    private boolean https = false;
    private boolean crud = true;
    private int port = 0;

    @Override
    public void init(WithKubernetesTestServer annotation) {
        this.https = annotation.https();
        this.crud = annotation.crud();
        this.port = annotation.port();
    }

    // ...
}

アノテーションを繰り返し使えるようにするには、包含するアノテーション型に @QuarkusTestResourceRepeatable を付ける必要があります。例えば、この場合、繰り返し使える @WithRepeatableTestResource アノテーションを定義することになります。

@QuarkusTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(WithRepeatableTestResource.List.class)
public @interface WithRepeatableTestResource {

    String key() default "";

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @QuarkusTestResourceRepeatable(WithRepeatableTestResource.class)
    @interface List {
        WithRepeatableTestResource[] value();
    }
}

14. ハング検出

@QuarkusTest は、予期しないハングを診断するために使用できるハング検出をサポートしています。指定された時間内に進捗がない場合 (つまり、JUnit コールバックが呼び出されない場合)、Quarkus はスタックトレースをコンソールに出力して、ハングの診断を助けます。このタイムアウトのデフォルト値は 10 分です。

これ以上のアクションは実行されず、テストは通常どおり (通常は CI がタイムアウトするまで) 続行されますが、出力されたスタックトレースは、ビルドが失敗した理由を診断するのに役立ちます。このタイムアウトは、quarkus.test.hang-detection-timeout システムプロパティーで制御できます (これは application.properties でも設定できますが、Quarkus が起動するまで読み取られないため、Quarkus の起動タイムアウトはデフォルトの 10 分です)。

15. ネイティブ実行可能ファイルテスト

@QuarkusIntegrationTest を使用してネイティブ実行可能ファイルをテストすることも可能です。これは、テストに注入すること (そして、ネイティブ実行可能ファイルは別の非 JVM プロセスで実行されることーこれは実際には可能ではありません) を除いて、このガイドで述べたすべての機能をサポートしています。

これについては、ネイティブ実行可能ファイルガイド で説明されています。

16. @QuarkusIntegrationTest の使用

@QuarkusIntegrationTest は、Quarkus ビルドによって生成されたアーティファクトを起動およびテストするために使用する必要があり、jar (タイプを問わず)、ネイティブイメージ、またはコンテナーイメージのテストをサポートします。簡単に言えば、Quarkus ビルド (mvn package または gradle build) の結果が jar である場合、その jar は java -jar …​ として起動され、それに対してテストが実行されることを意味します。ネイティブイメージがビルドされた場合、アプリケーションは ./application …​ として起動され、実行中のアプリケーションに対してテストが実行されます。最後に、ビルド中に ( quarkus-container-image-jib または quarkus-container-image-docker エクステンションを含め、 quarkus.container-image.build=true プロパティーを設定することで) コンテナーイメージが作成された場合)、コンテナーが作成されて実行されます (これには、docker 実行可能ファイルが存在する必要があります)。

これは、同じ機能セットをサポートし、同じ制限を持つブラックボックステストです。

@QuarkusIntegrationTest でアノテーションが付けられたテストはビルドの結果をテストするため、統合テストスイートの一部として実行する必要があります。 - 例えば、Maven を使用している場合は -DskipITs=false を設定し、Gradle を使用している場合は quarkusIntTest タスクを設定します。これらのテストは、Quarkus がまだ最終のアーティファクトを作成していないため、@QuarkusTest と同じフェーズで実行すると 機能しません

pom.xml ファイルには以下が含まれます。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
            <configuration>
                <systemPropertyVariables>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    <maven.home>${maven.home}</maven.home>
                </systemPropertyVariables>
            </configuration>
        </execution>
    </executions>
</plugin>

これは、failsafe-maven-plugin に統合テストを実行するように指示します。

次に、src/test/java/org/acme/quickstart/GreetingResourceIT.java を開きます。次の内容が含まれています:

package org.acme.quickstart;


import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest (1)
public class GreetingResourceIT extends GreetingResourceTest { (2)

    // Run the same tests

}
1 テストの前にネイティブファイルからアプリケーションを起動する別のテストランナーを使用します。実行可能ファイルは、Failsafe Maven Plugin によって取得されます。
2 便宜上、以前のテストを拡張していますが、独自のテストを実装することもできます。

詳細については、次のリンクを参照してください: building-native-image#testing-the-native-executable[ネイティブ実行可能ファイルのテストのガイド]

@QuarkusIntegrationTest を使ってアプリケーションをテストすると、 prod の設定プロファイルを使って起動しますが、これは quarkus.test.integration-test-profile プロパティを使って変更することができます。

While adding test-specific configuration properties using src/test/resources/application.properties (note there’s test, not main) is possible for unit tests, it’s not possible for integration tests.

16.1. コンテナーの起動

@QuarkusIntegrationTest によってコンテナーが起動されると (アプリケーションは`quarkus.container-image.build` が true に設定されているため)、コンテナーは予測可能なコンテナーネットワーク上で起動されます。これにより、アプリケーションをサポートするためにサービスを起動する必要がある統合テストの作成が容易になります。これは、@QuarkusIntegrationTest が、Dev Services を介して起動されたコンテナーでそのまま機能することを意味しますが、追加のコンテナーを起動する QuarkusTestLifecycleManager リソースを使用して有効化することも意味します。これは、QuarkusTestLifecycleManagerio.quarkus.test.common.DevServicesContext.ContextAware を実装することで実現できます。以下は、その簡単な例です。

テストするリソースを実行しているコンテナー (たとえば Testcontainers を介した PostgreSQL) には、コンテナーのネットワークから IP アドレスが割り当てられます。コンテナーのネットワークからの「パブリック」IP と「マップされていない」ポート番号を使用してサービスに接続します。Testcontainers ライブラリーは通常、コンテナーネットワークを尊重せずに接続文字列を返すため、コンテナーネットワーク上のコンテナーの IP と_マップされていない_ポート番号を使用して Quarkus に正しい接続文字列を提供するには、追加のコードが必要です。

次の例は PostgreSQL での使用法を示していますが、このアプローチはすべてのコンテナーに適用できます。

import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class CustomResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {

    private Optional<String> containerNetworkId;
    private JdbcDatabaseContainer container;

    @Override
    public void setIntegrationTestContext(DevServicesContext context) {
        containerNetworkId = context.containerNetworkId();
    }

    @Override
    public Map<String, String> start() {
        // start a container making sure to call withNetworkMode() with the value of containerNetworkId if present
        container = new PostgreSQLContainer<>("postgres:latest").withLogConsumer(outputFrame -> {});

        // apply the network to the container
        containerNetworkId.ifPresent(container::withNetworkMode);

        // start container before retrieving its URL or other properties
        container.start();

        String jdbcUrl = container.getJdbcUrl();
        if (containerNetworkId.isPresent()) {
            // Replace hostname + port in the provided JDBC URL with the hostname of the Docker container
            // running PostgreSQL and the listening port.
            jdbcUrl = fixJdbcUrl(jdbcUrl);
        }

        // return a map containing the configuration the application needs to use the service
        return ImmutableMap.of(
            "quarkus.datasource.username", container.getUsername(),
            "quarkus.datasource.password", container.getPassword(),
            "quarkus.datasource.jdbc.url", jdbcUrl);
    }

    private String fixJdbcUrl(String jdbcUrl) {
        // Part of the JDBC URL to replace
        String hostPort = container.getHost() + ':' + container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT);

        // Host/IP on the container network plus the unmapped port
        String networkHostPort =
            container.getCurrentContainerInfo().getConfig().getHostName()
            + ':'
            + PostgreSQLContainer.POSTGRESQL_PORT;

        return jdbcUrl.replace(hostPort, networkHostPort);
    }

    @Override
    public void stop() {
        // close container
    }
}

このドキュメントの対応するセクションで説明されているように、CustomResource@QuarkusTestResource を使用して @QuarkusIntegrationTest でアクティブ化されます。

16.2. 実行中のアプリケーションに対するテストの実行

@QuarkusIntegrationTest は、アプリケーションのすでに実行中のインスタンスに対するテストの実行をサポートします。これは、テストの実行時に quarkus.http.test-host システムプロパティーを設定することで実現できます。

次の Maven コマンドはその使用例で、@ QuarkusIntegrationTest を強制的に実行し、http://1.2.3.4:4321 からアクセスできます:

./mvnw verify -Dquarkus.http.test-host=1.2.3.4 -Dquarkus.http.test-port=4321

To test against a running instance that only accepts SSL/TLS connection (example: https://1.2.3.4:4321) set the system property quarkus.http.test-ssl-enabled to true.

17. @QuarkusTest と他のタイプのテストを混合

@QuarkusTest でアノテーションが付けられたテストと、@QuarkusDevModeTest@QuarkusProdModeTest@QuarkusUnitTest のいずれかでアノテーションが付けられたテストを、1 回の実行 (たとえば、1 回の Maven Surefire プラグインの実行) で混合することはできません。ただsじょ。後者の 3 つは共存できます。

この制限の理由は、@QuarkusTest がテスト実行の期間全体にわたって Quarkus サーバーを起動するため、他のテストが独自の Quarkus サーバーを起動できないようにするためです。

この制限を緩和するために、@QuarkusTest アノテーションは JUnit5 @Tag を定義します: io.quarkus.test.junit.QuarkusTest 。このタグを使用して、特定の実行 で @QuarkusTest テストを分離できます。以下は Maven Surefire プラグインを使用した例です。

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <executions>
        <execution>
            <id>default-test</id>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <excludedGroups>io.quarkus.test.junit.QuarkusTest</excludedGroups>
            </configuration>
        </execution>
        <execution>
            <id>quarkus-test</id>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <groups>io.quarkus.test.junit.QuarkusTest</groups>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <systemProperties>
            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
        </systemProperties>
    </configuration>
</plugin>

18. IDE から @QuarkusTest を実行する

ほとんどの IDE では、選択したクラスを JUnit テストとして直接実行できるようになっています。そのためには、選択した IDE の設定でいくつかのプロパティーを設定する必要があります:

  • java.util.logging.manager (ロギングガイド を参照)

  • maven.home (${maven.home}/conf/settings.xml にカスタム設定がある場合のみ)、Maven Guide を参照)

  • maven.settings (カスタム版の settings.xml ファイルをテストに使用する場合)

18.1. Eclipse の別個の JRE 定義

現在の"Installed JRE"定義を新しい定義にコピーし、新しいVMの引数としてプロパティーを追加します:

  • -Djava.util.logging.manager=org.jboss.logmanager.LogManager

  • -Dmaven.home=<path-to-your-maven-installation>

このJRE定義をQuarkusプロジェクトのターゲットランタイムとして使用すると、「Run as JUnit」設定に回避策が適用されます。

18.2. VSCode "run with" 設定

プロジェクトディレクトリーやワークスペースのルートにある settings.json には、テスト設定で次の回避策が必要です:

"java.test.config": [
    {
        "name": "quarkusConfiguration",
        "vmargs": [ "-Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dmaven.home=<path-to-your-maven-installation> ..." ],
        ...
    },
  ...
]

18.3. IntelliJ IDEA JUnit テンプレート

IntelliJ IDEA では何も必要ありません。なぜなら、IDEは systemPropertyVariables を `pom.xml`の surefire プラグイン設定から取得するからです。

19. 開発サービスのテスト

デフォルトでは、テストは Dev Services でのみ機能するはずですが、一部のユースケースでは、テストで自動的に設定されたプロパティーにアクセスする必要がある場合があります。

これは、@QuarkusTest または @QuarkusIntegrationTest に直接注入できる io.quarkus.test.common.DevServicesContext を使用して行うことができます。タイプ DevServicesContext のフィールドを定義するだけで、自動的に注入されます。これを使用して、設定されている任意のプロパティーを取得できます。通常、これは、テスト自体からリソースに直接接続するために使用されます。たとえば、kafka に接続して、テスト対象のアプリケーションにメッセージを送信するために使用されます。

io.quarkus.test.common.DevServicesContext.ContextAware を実装するオブジェクトへの注入もサポートされています。io.quarkus.test.common.DevServicesContext.ContextAware を実装するフィールドがある場合、Quarkus は` setIntegrationTestContext` メソッドを呼び出して、コンテキストをこのオブジェクトに渡します。これにより、クライアントロジックをユーティリティークラスにカプセル化できます。

QuarkusTestResourceLifecycleManager 実装は、ContextAware を実装してこれらのプロパティーにアクセスすることもできます。これにより、Quarkus が起動する前にリソースを設定できます (たとえば、KeyCloak インスタンスの設定、データベースへのデータの追加など)。

アプリケーションをコンテナーとしてランチャーにする @QuarkusIntegrationTest テストの場合、io.quarkus.test.common.DevServicesContext は、アプリケーションコンテナーが起動されたコンテナーネットワークの ID へのアクセスも提供します (containerNetworkId メソッドを使用)。これは、アプリケーションが通信する追加のコンテナーを起動する必要がある QuarkusTestResourceLifecycleManager で使用できます。

20. Testing Components

This feature is experimental and the API may change in the future.

In Quarkus, the component model is built on top CDI. Therefore, Quarkus provides the QuarkusComponentTestExtension, a JUnit extension to ease the testing of components and mocking of their dependencies. This extension is available in the quarkus-junit5-component dependency.

Let’s have a component 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 is an @ApplicationScoped CDI bean.
2 Foo depends on Charlie which declares a method ping().
3 Foo depends on the config property bar.

Then a component test could look like:

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 The QuarkusComponentTest annotation registers the JUnit extension.
2 Sets a configuration property for the test.
3 The test injects the component under the test. The types of all fields annotated with @Inject are considered the component types under test. You can also specify additional component classes via @QuarkusComponentTest#value(). Furthermore, the static nested classes declared on the test class are components too.
4 The test also injects Charlie, a dependency for which a synthetic @Singleton bean is registered automatically. The injected reference is an "unconfigured" Mockito mock.
5 We can leverage the Mockito API in a test method to configure the behavior.

QuarkusComponentTestExtension also resolves parameters of test methods and injects matching beans. So the code snippet above can be rewritten as:

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 Parameters annotated with @io.quarkus.test.component.SkipInject are never resolved by this extension.

Furthermore, if you need the full control over the QuarkusComponentTestExtension configuration then you can use the @RegisterExtension annotation and configure the extension programatically. The original test could be rewritten like:

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 The QuarkusComponentTestExtension is configured in a static field of the test class.

20.1. Lifecycle

So what exactly does the QuarkusComponentTest do? It starts the CDI container and registers a dedicated configuration object. If the test instance lifecycle is Lifecycle#PER_METHOD (default) then the container is started during the before each test phase and stopped during the after each test phase. However, if the test instance lifecycle is Lifecycle#PER_CLASS then the container is started during the before all test phase and stopped during the after all test phase. The fields annotated with @Inject and @InjectMock are injected after a test instance is created. Finally, the CDI request context is activated and terminated per each test method.

20.2. インジェクション

Test class fields annotated with @jakarta.inject.Inject and @io.quarkus.test.InjectMock are injected after a test instance is created. Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed. Parameters of a test method for which a matching bean exists are resolved unless annotated with @io.quarkus.test.component.SkipInject. Dependent beans injected into the test method arguments are correctly destroyed after the test method completes.

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.

20.3. Auto Mocking Unsatisfied Dependencies

Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency. Instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency. The bean has the @Singleton scope so it’s shared across all injection points with the same required type and qualifiers. The injected reference is an unconfigured Mockito mock. You can inject the mock in your test using the io.quarkus.test.InjectMock annotation and leverage the Mockito API to configure the behavior.

20.4. Custom Mocks For Unsatisfied Dependencies

Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. You can use the mock configurator API via the QuarkusComponentTestExtensionBuilder#mock() method.

20.5. 設定

You can set the configuration properties for a test with the @io.quarkus.test.component.TestConfigProperty annotation or with the QuarkusComponentTestExtensionBuilder#configProperty(String, String) method. If you only need to use the default values for missing config properties, then the @QuarkusComponentTest#useDefaultConfigProperties() or QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties() might come in useful.

It is also possible to set configuration properties for a test method with the @io.quarkus.test.component.TestConfigProperty annotation. However, if the test instance lifecycle is Lifecycle#_PER_CLASS this annotation can only be used on the test class and is ignored on test methods.

CDI beans are also automatically registered for all injected Config Mappings. The mappings are populated with the test configuration properties.

20.6. Mocking CDI Interceptors

If a tested component class declares an interceptor binding then you might need to mock the interception too. There are two ways to accomplish this task. First, you can define an interceptor class as a static nested class of the test class.

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 is an interceptor binding.
2 The interceptor class is automatically considered a tested component.
Static nested classed 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.

Furthermore, you can also declare a "test interceptor method" directly on the test class. This method is then invoked in the relevant interception phase.

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 The interceptor bindings of the resulting interceptor are specified by annotating the method with the interceptor binding types.
2 Defines the interception type.

関連コンテンツ