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.15

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

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

  • 入門ガイド の完了した greeter アプリケーション

2. アーキテクチャ

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

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

3. ソリューション

次のセクションの手順に従って、アプリケーションを段階的に作成することをお勧めします。 ただし、完成した例に直接進むこともできます。

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

ソリューションは getting-started-testing ディレクトリ にあります。

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

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

「はじめに」の例から開始した場合、正しいツール設定を含む、完了したテストがすでにあるはずです。

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

Maven
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit</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-junit")
    testImplementation("io.rest-assured:rest-assured")
}

quarkus-junit は、テストフレームワークを制御する @QuarkusTest アノテーションを提供するため、テストに必要です。 rest-assured は必須ではありませんが、HTTP エンドポイントをテストするのに便利な方法です。また、正しい URL を自動的に設定するインテグレーションも提供しているため、設定は不要です。

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

<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 システムプロパティーを設定し、 ${maven.home}/conf/settings.xml からのカスタム設定 (存在する場合) が適用されるように maven.home も設定します。

プロジェクトにはシンプルなテストも含まれている必要があります。

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.properties で HTTP の場合は quarkus.http.test-port を、HTTPS の場合は quarkus.http.test-ssl-port を設定することにより、テストで使用されるポートを構成できます。

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 アノテーションを介して行われます。

これを活用して、静的リソースをロードするシンプルなテストを書いてみましょう。まず、src/main/resources/META-INF/resources/index.html にシンプルな 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 の URIURL、および String 表現をインジェクションできます。

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

RESTassured と @TestHTTPResource の両方で、パスをハードコーディングするのではなく、テストするエンドポイントクラスを指定できます。これは現在、Jakarta REST エンドポイント、サーブレット、およびリアクティブルートの両方をサポートしています。これにより、特定のテストがどのエンドポイントをテストしているのかを正確に把握することがはるかに容易になります。

これらの例のために、以下のようなエンドポイントがあると仮定します。

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

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

5.1. TestHTTPResource

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 アノテーションを使用できます。これはクラスレベルまたはメソッドレベルで適用できます。greeting リソースをテストするには、次のようにします。

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 がテストに注入されます。
@SessionScoped Bean を注入/テストしたい場合、セッションコンテキストがアクティブではない可能性が高く、注入された Bean のメソッドが呼び出されたときに ContextNotActiveException を受け取ることになります。ただし、 @io.quarkus.test.ActivateSessionContext インターセプターバインディングを使用して、特定のビジネスメソッドのセッションコンテキストをアクティブ化することは可能です。さらなる制限については、javadoc を参照してください。

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
コールバックが何をするかを制御するために、テストクラスやメソッドからアノテーションを読み取ることが可能です。

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

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

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

Quarkus の再起動回数を減らすために、 io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer は、 JUnit User Guide に記載されているように、グローバル ClassOrderer として登録されます。この ClassOrderer の動作は、 quarkus.test.class-orderer プロパティーを使用して application.properties 経由で設定できます。Quarkus の ClassOrderer は、同じアプリケーションインスタンスを必要とするテストがグループ化されるという制約のもと、セカンダリーオーダーに従ってソートを試みます。このプロパティーは、セカンダリーオーダーとして使用する ClassOrderer の FQCN を受け入れます。JUnit が提供するオーダーや、独自のカスタムオーダーに設定できます。クラスが見つからない場合は、 ClassOrderer をまったく設定しない JUnit のデフォルトの動作にフォールバックします。

Quarkus のオーダーは、JUnit の設定を使用して別の ClassOrderer を設定することで、完全に無効にすることもできます。これは、すべての Quarkus テストが同じアプリケーション上で実行される場合 (つまり、テストプロファイルとテストリソースの使用が制限される場合) にのみ許可されます。

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 テストプロファイルの実装で CDI Bean が宣言されている場合 (プロデューサーメソッド/フィールドまたはネストされた静的クラス経由)、この Bean はテストプロファイルが使用されている場合にのみ考慮され、他のテストプロファイルでは無視されます。

これでプロファイルを定義したので、それをテストクラスに含める必要があります。そのためには、テストクラスに @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. ネストしたテスト

JUnit の @Nested テスト は、より複雑なテストシナリオを構成するのに役立ちます。ただし、同じ親クラス内のネストされたテストに異なるテストプロファイルやリソースを割り当てることはできないことに注意してください。

12. モックサポート

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

12.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 ディレクトリーに存在することが重要です。そうしないと、テスト時だけでなく常に有効になってしまいます。

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

12.2. QuarkusMock を使用したモック

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

このメソッドは、通常のスコープを持つ 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 注入された Bean を置き換えるために installMockForInstance を使用します。これはテストメソッドの期間中有効になります。

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

@Inject を使用すると、インストールしたモックインスタンスへの CDI プロキシーが取得されますが、これはモックインスタンス自体を必要とする Mockito.verify などのメソッドに渡すのには適していません。したがって、verify などのメソッドを呼び出す必要がある場合は、テストでモックインスタンスを保持するか、@io.quarkus.test.InjectMock を使用する必要があります。

12.2.1. @InjectMock でのさらなる単純化

QuarkusMock が提供する機能を基に、Quarkus はユーザーが Mockito を簡単に利用して、QuarkusMock でサポートされている Bean をモックすることも可能にします。

この機能は、quarkus-junit-mockito 依存関係が存在する 場合にのみ@io.quarkus.test.InjectMock アノテーションで利用可能です:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit-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 のレスポンスの代わりに、モックされたレスポンスを取得します。

デフォルトでは、@InjectMock アノテーションは、通常の CDI スコープを持つ Bean (例: @ApplicationScoped@RequestScoped) に使用できます。@Singleton Bean のモックは、@MockitoConfig(convertScopes = true) アノテーションを追加することで実行できます。これにより、テストのために @Singleton Bean が @ApplicationScoped Bean に変換されます。

これは高度なオプションと見なされ、Bean のスコープを変更することによって生じる影響を完全に理解している場合にのみ実行してください。

12.2.2. @InjectSpy でモックの代わりにスパイを使用

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

テストの際、特定の論理パスが通ったことを確認するだけでよい場合や、スパイされたクローン上の残りのメソッドを実行したまま、単一のメソッドのレスポンスをスタブアウトするだけでよい場合があります。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 Spy からモックされたレスポンスが返されることを検証しています。
@InjectMock の場合とは異なり、CDI オブザーバーを使用する Bean に対して @InjectSpy を使用すると、これらのオブザーバーは無効にならず、通常の CDI ルールに従って実行されます。

12.2.3. @InjectMock@RestClient の併用

@RegisterRestClient は実行時に REST Client の実装を登録します。Bean は通常のスコープである必要があるため、インターフェイスに @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 のインスタンスを使用することを意味していることを示します。

12.3. 起動時のモック

起動時にアクセスおよび使用される Bean は、テストが @InjectMock または @InjectSpy でアノテーションが付けられた同じ Bean タイプを持つインジェクションポイントを宣言しても、自動モックの対象にはなりません。モックはテスト内のローカルな操作ですが、起動は多数のテストを実行するためのグローバルな操作です。このような場合、CDI @Alternative メカニズム を使用することをお勧めします。

12.4. Panache を使用したモック

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

13. セキュリティのテスト

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

14. Quarkus アプリケーション起動前のサービス開始

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

テストスイート内の任意のテストに @QuarkusTestResource アノテーションを付けるだけで、Quarkus はテストが実行される前に、対応する QuarkusTestResourceLifecycleManager を実行します。テストスイートでは、複数の @QuarkusTestResource アノテーションを自由に利用することもでき、その場合、対応するすべての QuarkusTestResourceLifecycleManager オブジェクトがテストの前に実行されます。

テストリソースはグローバルであり、テストクラスやカスタムプロファイルで定義されていても、重複を削除してもすべてのテストに対してアクティブ化されます。単一のテストクラスまたはテストプロファイルでのみテストリソースを有効にしたい場合は、@QuarkusTestResource(restrictToAnnotatedClass = true) を使用できます。
複数のテストリソースを使用する場合、それらを同時に開始できます。そのためには、@QuarkusTestResource(parallel = true) を設定する必要があります。

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

QuarkusTestResourceLifecycleManager は CDI Bean ではないため、これを実装するクラスには @Inject で注入されたフィールドを含めることはできません。String propertyName = ConfigProvider.getConfig().getValue("quarkus.my-config-group.myconfig", String.class); を使用できます。

14.1. テストクラスの変更

テストクラスに何かを注入する必要があるカスタムの QuarkusTestResourceLifecycleManager を作成する場合、 inject メソッドを使用できます。例えば、次のようなテストがある場合です。

@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 がテストクラスへの必要な注入を実行した後に発生することに留意してください。

14.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.3. @WithTestResource の使用

@QuarkusTestResource によって提供されるテストリソースは、グローバルに利用できるか、アノテーションが付けられたテストクラス (restrictToAnnotatedClass) に制限されますが、アノテーション @WithTestResource を使用すると、実行用にテストリソース別にテストをさらにグループ化できます。@WithTestResource には、TestResourceScope 列挙値を取る scope プロパティーがあります。

  • TestResourceScope.MATCHING_RESOURCES (デフォルト): Quarkus は同じテストリソースを持つテストをグループ化し、まとめて実行します。グループの実行後、すべてのテストリソースは停止され、次のグループが実行されます。

  • TestResourceScope.RESTRICTED_TO_CLASS: テストリソースは、アノテーションが付けられたテストクラスでのみ使用でき、テストクラスの実行後に停止されます。

  • TestResourceScope.GLOBAL: テストリソースはテストスイート内のすべてのテストに適用されます

次のいずれかに該当する場合、Quarkus は再起動する必要があります。

  • 現在のテストのテストリソースの少なくとも1つが、そのテストクラスに制限されている場合

  • 次のテストのテストリソースの少なくとも1つが、そのテストクラスに制限されている場合

  • 異なる MATCHING_RESOURCES スコープのテストリソースが使用されている場合

15. ハング検出

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

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

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

@QuarkusIntegrationTest を使用してネイティブ実行可能ファイルをテストすることも可能です。これは、テストへの注入 (および、ネイティブ実行可能ファイルが別の非 JVM プロセスで実行されるため、これは実際には不可能です) を除いて、このガイドで言及されているすべての機能をサポートします。

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

17. @QuarkusIntegrationTest の使用

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

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

@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 便宜上、以前のテストを拡張していますが、独自のテストを実装することもできます。

詳細については、ネイティブ実行可能ファイルテストガイド を参照してください。

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

src/test/resources/application.properties を使用してテスト固有の設定プロパティーを追加することは ( main ではなく test があることに注意)、ユニットテストでは可能ですが、統合テストでは不可能です。

17.1. @QuarkusIntegrationTest のデバッグ

失敗した @QuarkusInterationTest のデバッグは、Quarkus アプリケーションが別のプロセスとして起動されるため、@QuarkusTest のデバッグよりも困難です。

テストコード自体は、よく知られているどの方法でも簡単にデバッグできます (IDE からテストを起動したり、Maven 経由でテストを起動する場合は -Dmaven.failsafe.debug を追加したりするなど)。しかし、テスト対象のアプリケーションは、デバッグを有効にするために quarkus.test.arg-line 経由で必要なフラグを渡す必要があります。 jar としてパッケージ化されたアプリケーションをテストする場合は、次を使用できます。

-Dquarkus.test.arg-line="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"

テストを起動するときに Maven/Gradle への引数として指定します。これにより、リモート JVM デバッガーがポート 5005 に接続されるまでアプリケーションが待機するようになります。

17.2. インテグレーションテスト中の AOT キャッシュの生成

JDK 25 以降をターゲットとする場合、 @QuarkusIntegrationTest テストの実行中に Quarkus に AOT キャッシュ (app.aot) を生成するように指示できます。これを有効にするには、統合テストの実行に次のシステムプロパティーを追加します。

-Dquarkus.package.jar.aot.enabled=true

このフラグを使用すると、テスト対象のアプリケーションは JVM の AOT キャッシュ出力オプションで起動され、ビルドされた jar の横に app.aot ファイルが生成されます (例: target/quarkus-app/app.aot)。その後、アプリケーションを次のように実行できます。

java -XX:AOTCache=app.aot -jar quarkus-run.jar

この機能は本質的に、既存のテストスイートをトレーニング実行に変えます。

  • この機能には JDK 25 以降が必要です。

  • 生成されたキャッシュの有効性は、インテグレーションテストが本番環境の起動と初期トラフィックをどれだけ代表しているかに依存します。

17.3. コンテナーの起動

@QuarkusIntegrationTest によりコンテナーが起動されると (アプリケーションが quarkus.container-image.buildtrue に設定してビルドされたため)、コンテナーは予測可能なコンテナーネットワーク上で起動されます。これにより、アプリケーションをサポートするサービスを起動する必要がある統合テストの作成が容易になります。これは、@QuarkusIntegrationTestDev 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<>("docker.io/library/postgres:18").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 でアクティブ化されます。

17.4. 実行中のアプリケーションに対する実行

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

この使用例として、 @QuarkusIntegrationTesthttp://1.2.3.4:4321 でアクセス可能なインスタンスに対して実行するように強制する、以下の Maven コマンドが挙げられます:

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

SSL/TLS 接続のみを受け入れる実行中のインスタンス (例: https://1.2.3.4:4321) に対してテストするには、システムプロパティー quarkus.http.test-ssl-enabledtrue に、quarkus.http.test-ssl-port をターゲットの HTTPS ポートに設定します。

18. @QuarkusTest と他の種類のテストの混合

Mixing tests annotated with @QuarkusTest with tests annotated with either @QuarkusDevModeTest, @QuarkusProdModeTest or @QuarkusExtensionTest is not allowed in a single execution run (in a single Maven Surefire Plugin execution, for instance), while the latter three can coexist.

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

この制限を緩和するために、 @QuarkusTest アノテーションは JUnit @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>

現在、 @QuarkusTest@QuarkusIntegrationTest を同じテストランで実行しないでください。

Maven の場合、前者は surefire プラグインによって、後者は failsafe プラグインによって実行される必要があることを意味します。

Gradle の場合、これは 2 種類のテストが異なるソースセットに属する必要があることを意味します。

ソースセットの設定例
/** custom source sets
 *
 * io.quarkus.gradle.QuarkusPlugin
 * io.quarkus.test.junit.IntegrationTestUtil
 *
 * to work around
 *  https://github.com/quarkusio/quarkus/issues/43796
 *  https://github.com/quarkusio/quarkus/issues/43804
 */

sourceSets {
    create("intTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
    create("e2eTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

configurations {
    getByName("intTestImplementation") {
        extendsFrom(configurations.testImplementation.get())
    }
    getByName("intTestRuntimeOnly") {
        extendsFrom(configurations.testRuntimeOnly.get())
    }
    getByName("e2eTestImplementation") {
        extendsFrom(configurations.testImplementation.get())
    }
    getByName("e2eTestRuntimeOnly") {
        extendsFrom(configurations.testRuntimeOnly.get())
    }
}

tasks.register<Test>("intTest") {
    group = "verification"
    description = "Runs integration tests"

    testClassesDirs = sourceSets["intTest"].output.classesDirs
    classpath = sourceSets["intTest"].runtimeClasspath

    systemProperty("build.output.directory", "build")
    systemProperty("quarkus.profile", "intTest")
}

tasks.register<Test>("e2eTest") {
    group = "verification"
    description = "Runs e2e tests"

    val quarkusBuild = tasks.getByName("quarkusBuild")
    dependsOn(quarkusBuild)

    testClassesDirs = sourceSets["e2eTest"].output.classesDirs
    classpath = sourceSets["e2eTest"].runtimeClasspath

    systemProperty("build.output.directory", "build")
}

idea.module {
    testSources.from(sourceSets["intTest"].kotlin.srcDirs)
    testSources.from(sourceSets["e2eTest"].kotlin.srcDirs)
}

19. IDE から @QuarkusTest の実行

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

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

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

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

19.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」設定に回避策が適用されます。

19.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> ..." ],
        ...
    },
  ...
]

19.3. IntelliJ IDEA JUnit テンプレート

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

20. Dev Services のテスト

デフォルトでは、テストは 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 で使用できます。

21. @QuarkusTest メタアノテーションの作成

@QuarkusTest アノテーションは、カスタムメタアノテーションを定義することで、他のアノテーションとグループ化できます。たとえば、以下の例のように、 @Tag アノテーションと組み合わせて、テストのグループを自動的にマークできます:

org.acme.test

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@QuarkusTest
@Tag("security")
public @interface QuarkusSecurityTest {   (1)
}
1 @QuarkusSecurityTest アノテーションは、 @QuarkusTest アノテーションの代わりに使用できます。

22. コンポーネントのテスト

Quarkus は、コンポーネントのテストと依存関係のモック作成を容易にする JUnit エクステンションである QuarkusComponentTestExtension を提供します。この JUnit エクステンションは、 quarkus-junit-component 依存関係で利用できます。

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 を活用して動作を設定できます。

さらに多くの例とヒントについては、 テストコンポーネントリファレンスガイド を参照してください。

関連コンテンツ