アプリケーションのテスト
Quarkusアプリケーションのテスト方法について説明します。このガイドでは、以下の内容について説明します。
-
JVM モードでのテスト
-
ネイティブモードでのテスト
-
テストへのリソースの注入
1. 前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 11+ がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.8.6
-
使用したい場合は、 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 つのテスト依存関係が表示されます。
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
システムプロパティーを設定し、${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
をリッスンします。これにより、アプリケーションを並行して実行しながらテストを実行することができます。
テストポートの変更
|
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
の両方で、パスをハードコーディングするのではなく、テストするエンドポイントクラスを指定することができます。これは現在、JAX-RS エンドポイント、サーブレット、リアクティブルートの両方をサポートしています。これにより、特定のテストがどのエンドポイントをテストしているかを正確に確認することが非常に簡単になりました。
これらの例では、以下のようなエンドポイントを想定しています:
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
これは現在、JAX-RS のコンテキストパスを設定するための @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 javax.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-tests
を true
とすることで、これらのコールバックを @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
テストクラスやメソッドからアノテーションを読み込んで、コールバックが何をするかを制御することができます。 |
BeforeEachCallback のような JUnit Jupiter コールバックインターフェイスを使うことも可能ですが、QuarkusはJUnitが把握しないカスタムクラスローダーでテストを実行する必要がある為、クラスローディングの問題にぶつかるかもしれません。
|
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 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 JAX-RS 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;
}
}
1 | これらのメソッドにはすべてデフォルトの実装があるため、オーバーライドが必要なメソッドのみオーバーライドします。 |
これでプロファイルを定義したので、それをテストクラスに含める必要があります。そのためには、テストクラスに @TestProfile(MockGreetingProfile.class)
アノテーションを付けます。
テストプロファイルの設定はすべて単一のクラスに保存されているので、前回のテストが同じ設定で実行されたかどうかが簡単にわかります。
10.2. 特定のテストの実行
Quarkus は、テストの実行を特定の @TestProfile
アノテーションを持つテストに制限できます。これは、QuarkusTestProfile
の tags
メソッドを 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 Collections.singleton("test1");
}
}
public static class MultipleTags implements QuarkusTestProfile {
@Override
public Set<String> tags() {
return new HashSet<>(Arrays.asList("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
の値と一致するため、SingleTagTest
とMultipleTagsTest
が実行されます。 -
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には依存しないことに注意してください。好きなモッキングライブラリを使うことができますし、必要な動作を提供するためにオブジェクトを手動でオーバーライドすることもできます。
@Inject を使用すると、インストールしたモックインスタンスへの CDI プロキシーを取得できます。これは、モックインスタンス自体が必要な Mockito.verify などのメソッドに渡すのには適していません。したがって、verify などのメソッドを呼び出す必要がある場合は、テストでモックインスタンスを保持するか、以下のように @InjectMock を使用する必要があります。
|
11.2.1. @InjectMock
での更なる単純化
QuarkusMock
で提供されている機能をベースに、Quarkusでは、 QuarkusMock
でサポートされているBeanをモックするために Mockito を簡単に利用できるようにしています。この機能は、 quarkus-junit5-mockito
依存関係で利用可能な @io.quarkus.test.junit.mockito.InjectMock
アノテーションを介して利用できます。
@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 により、モックがテストクラスのテストメソッドに存在することになり、利用可能になります (他のテストクラスはこの影響を受け ません )。 |
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 のモックは、convertScopes
プロパティーを true に設定することで実行できます (@InjectMock (convertScopes = true
など)。これにより、@Singleton
Bean がテスト用の @ApplicationScoped
Bean に変換されます。.
これは高度なオプションと見なされ、Bean のスコープを変更することで引き起こされる結果を完全に理解している場合にのみ実行する必要があります。
11.2.2. @InjectSpy
で、モックの代わりにスパイを使用する
InjectMock
で提供されている機能をベースに、 QuarkusMock
でサポートされているBeanをスパイするために Mockito を簡単に利用できるようにしました。この機能は、 quarkus-junit5-mockito
依存関係で利用可能な @io.quarkus.test.junit.mockito.InjectSpy
アノテーションを介して利用できます。
テストを行う際に、特定の論理パスが取られたかどうかを確認するだけで済む場合もありますし、Spied クローン上で残りのメソッドを実行している間に、1つのメソッドのレスポンスをスタブアウトするだけで済む場合もあります。Spy パーシャル モックの詳細については Mockito のドキュメントを参照してください。いずれの場合も、オブジェクトの 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() {
when(greetingService.greet()).thenReturn("hi"); (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 | ここでは、"hello"の代わりに"hi"を返すようにSpyに指示しています。 GreetingResource が GreetingService から挨拶を要求するとき、通常の GreetingService Bean のレスポンスの代わりにモックされたレスポンスを取得します。 |
3 | 私たちは、スパイからのモックされた応答を得ることを検証しています。 |
11.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 のインスタンスを使用することを意味していることを示します。 |
11.3. Mocking with Panache
quarkus-hibernate-orm-panache
や quarkus-mongodb-panache
のエクステンションを使っている場合は、Hibernate ORM と Panache モック および MongoDB と Panache モック のドキュメントをチェックして、データアクセスをモックする最も簡単な方法を確認してください。
12. セキュリティーのテスト
Quarkus Securityを使用している場合、アプリケーションのセキュリティー機能を簡単にテストする方法については、セキュリティーのテスト のセクションをご覧ください。
13. Quarkus アプリケーションの開始前にサービスを開始する
非常に一般的なニーズは、Quarkusアプリケーションがテストを開始する前に、Quarkusアプリケーションに依存するいくつかのサービスを開始することです。このニーズに対応するために、Quarkusでは、 @io.quarkus.test.common.QuarkusTestResource
と io.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 サーバの起動 (その例は こちら を参照) などがあります。
13.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
メソッドに示すように、MyWireMockResource
に wireMockServer
フィールドを注入させることができます。
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();
}
// ...
}
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
実行可能ファイルが存在する必要があります)。
@NativeImageTest
の場合と同様に、これは同じセットの機能をサポートし、同じ制限があるブラックボックステストです。
|
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[ネイティブ実行可能ファイルのテストのガイド]
16.1. コンテナーの起動
@QuarkusIntegrationTest
によってコンテナーが起動されると (アプリケーションは quarkus.container-image.build
が true
に設定されているため)、コンテナーは予測可能なコンテナーネットワーク上で起動されます。これにより、アプリケーションをサポートするためにサービスを起動する必要がある統合テストの作成が容易になります。これは、@QuarkusIntegrationTest
が、Dev Services を介して起動されたコンテナーでそのまま機能することを意味しますが、追加のコンテナーを起動する QuarkusTestLifecycleManager リソースを使用して有効化することも意味します。これは、QuarkusTestLifecycleManager
に io.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. 実行中のアプリケーションに対するテストの実行
この機能は実験的なものと見なされており、Quarkus の将来のバージョンで変更される可能性があります。 |
@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
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」設定に回避策が適用されます。
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 インスタンスの設定、データベースへのデータの追加など)。
アプリケーションをコンテナーとしてランチャーにする |