Spring Boot REST APIのテストには、複数のスコープが必要です。直接的なJava呼び出し、シミュレートされたMVCリクエスト、稼働中のサーバーを介したリクエストは、それぞれ異なる障害境界を検証するためです。効果的なテストスイートでは、ほとんどの振る舞いのチェックをSpringコンテキストの外側に置き、その上で、より狭いWebスライスと少数の完全なアプリケーションテストを追加します。
依存関係の例は、テストスターターがJUnit 5を使用するSpring Boot 3.xを対象としています。Spring Boot 4ではJUnit 6が管理されます。Jupiterの概念は引き続き使用できますが、いくつかのBootテストアノテーションではパッケージ名が変更されています。
テストフレームワークを使わないテスト #
REST APIは、JUnit、Spring Test、Mockitoを使わずに検証できます。アプリケーションを起動し、JDK HTTPクライアントでリクエストを送信し、レスポンスがコントラクトに違反した場合にエラーをスローします。
package com.example;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public final class ManualApiCheck {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/greetings/Ada"))
.header("Accept", "application/json")
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new AssertionError("status: " + response.statusCode());
}
if (!response.body().equals("{\"message\":\"Hello, Ada\"}")) {
throw new AssertionError("body: " + response.body());
}
}
}Javaのassert文で明示的なチェックを置き換えることもできますが、JVMを-ea付きで起動しない限りアサーションは無効です。そのため、明示的なAssertionErrorを使うと、信頼できるプロセス終了コードを得られます。
カスタム実行可能プログラムは、すでにデプロイされたサービスに対するスモークチェックに有用です。ただし、テストフレームワークを完全に置き換えるには、テストの検出、フィクスチャのライフサイクル、分離、結果の報告、並列実行、ビルド統合を実装する必要があります。手動のcurlまたはブラウザによるチェックは、反復可能な回帰テストスイートではなく、探索を提供します。
テストスコープの選択 #
Springのテストドキュメントでは、単体テストと統合テストを区別し、複数のWebテストスコープを提供しています。すべてのエンドポイントに推奨される単一のアノテーションはありません。検証対象の振る舞いを含む最小のスコープを選択してください。
| スコープ | メカニズム | 検証される振る舞い |
|---|---|---|
| 通常の単体テスト | JUnitと通常のコンストラクター | Springを使わないコントローラーまたはサービスのJavaロジック |
| MVCスライス | @WebMvcTestとMockMvc | ルーティング、バインディング、バリデーション、シリアライゼーション、フィルター、コントローラーアドバイス |
| モックWebレイヤー付き完全コンテキスト | @SpringBootTestと@AutoConfigureMockMvc | アプリケーションの配線とシミュレートされたServletリクエスト |
| 完全な稼働サーバー | @SpringBootTest(webEnvironment = RANDOM_PORT) | 組み込みサーバー、フィルター、シリアライゼーション、ネットワーク、完全なアプリケーション配線 |
| デプロイ済みシステムのテスト | 外部HTTPクライアント | パッケージ化されたサービスとデプロイ済み依存関係 |
Springは、Springコンテナーが不要な場合は単体テストを推奨しています。そのうえでMockMvcは、稼働中のサーバーなしにSpring MVCの完全なリクエスト処理を提供します。ただし、シミュレートされたリクエストではServletコンテナーのネットワーク、ディスパッチ、エラーページの振る舞いを検証できないため、稼働サーバーを使うテストもいくつか保持してください。
データベースまたはブローカーを使用するテストでは、別途依存関係に関する判断が必要です。
- アプリケーションの判断をテストする場合は、境界をフェイクまたはモックに置き換えます。
- データベースクエリ、マッピング、トランザクション、ブローカーの振る舞い、またはワイヤー互換性が要件に含まれる場合は、分離された実サービスを使用します。
- 自動テストから本番インフラストラクチャを参照してはいけません。
Spring Bootのテスト依存関係 #
アプリケーションがすでにSpring Boot 3の親を継承しているか、BOMをインポートしている場合、必要なテスト依存関係は1つです。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>このスターターには、Spring Boot Test、Spring Test、JUnit Jupiter、Mockito、AssertJ、Hamcrest、JSONassert、JsonPath、Awaitilityが含まれます。依存関係セットは1つのリリーストレインとしてテストされているため、バージョンはSpring Bootに管理させてください。
以下のアプリケーション境界を例で使用します。
public interface GreetingService {
Greeting greet(String name);
}
public record Greeting(String message) {}
@RestController
@RequestMapping("/greetings")
public final class GreetingController {
private final GreetingService service;
public GreetingController(GreetingService service) {
this.service = service;
}
@GetMapping("/{name}")
Greeting get(@PathVariable String name) {
return service.greet(name);
}
}通常のJUnit 5テスト #
Springコンポーネントは通常のJavaオブジェクトのままです。テストが分岐、返却値、またはサービスへの委譲を対象とする場合、コントローラーを直接構築するとコンテキストの起動を回避できます。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class GreetingControllerTest {
@Test
void returnsTheServiceResult() {
GreetingService service =
name -> new Greeting("Hello, " + name);
var controller = new GreetingController(service);
assertEquals(
new Greeting("Hello, Ada"),
controller.get("Ada"));
}
}このテストでは、直接メソッド呼び出しがSpring MVCを迂回するため、@GetMapping、パス変数の変換、JSONシリアライゼーション、バリデーション、フィルター、例外ハンドラーは検証されません。
JUnit 5によるMVCスライステスト #
@WebMvcTestは、アプリケーション全体ではなく、MVCインフラストラクチャと選択したWebコンポーネントを読み込みます。Mockitoをテストから排除するため、コントローラーの依存関係には決定論的な実装を提供します。
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(GreetingController.class)
@Import(GreetingControllerWebTest.Dependencies.class)
class GreetingControllerWebTest {
@Autowired
MockMvc mvc;
@Test
void rendersAJsonGreeting() throws Exception {
mvc.perform(get("/greetings/Ada"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(
"application/json"))
.andExpect(jsonPath("$.message").value("Hello, Ada"));
}
@TestConfiguration(proxyBeanMethods = false)
static class Dependencies {
@Bean
GreetingService greetingService() {
return name -> new Greeting("Hello, " + name);
}
}
}その振る舞いがエンドポイントのコントラクトに属する場合は、セキュリティ設定、コントローラーアドバイス、コンバーター、フィルターをスライスに追加します。テスト対象の本番コンポーネントを除外したスライスは、組み立てられたアプリケーションが失敗する場合でも成功する可能性があります。そのため、完全なコンテキストによるカバレッジも必要です。
完全なコンテキストテスト #
デフォルトの@SpringBootTest Web環境はMOCKです。@AutoConfigureMockMvcを追加すると、ネットワークポートを開かずに完全なアプリケーションコンテキストを通じてリクエストを実行できます。
@SpringBootTest
@AutoConfigureMockMvc
class GreetingApplicationTest {
@Autowired
MockMvc mvc;
@Test
void servesAGreeting() throws Exception {
mvc.perform(get("/greetings/Ada"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Hello, Ada"));
}
}組み込みサーバーまたは実際のHTTPの振る舞いがテスト対象に含まれる場合は、ランダムポート環境を使用します。
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingHttpTest {
@LocalServerPort
int port;
@Test
void servesAGreetingOverHttp() throws Exception {
var request = HttpRequest.newBuilder()
.uri(URI.create(
"http://localhost:" + port + "/greetings/Ada"))
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertEquals(
"{\"message\":\"Hello, Ada\"}",
response.body());
}
}ランダムポートは、並列ビルド中の固定ポートの競合を防ぎます。クライアントコードとサーバーコードは別スレッドで実行されるため、テストスレッドで開始したトランザクションが、稼働サーバーのリクエストによってコミットされたデータを自動的にロールバックすることはありません。状態を明示的にリセットするか、バッキングサービスを分離してください。
Boot 3にはTestRestTemplateも用意されており、SpringのリアクティブスタックにはWebTestClientがあります。Spring Framework 7では、MockMvcと稼働サーバーテスト全体で共通の同期アサーションAPIを提供するRestTestClientが追加されます。バージョンが指定されていない例をコピーするのではなく、アプリケーションで使用しているSpringのバージョンがサポートするクライアントを選択してください。
SpringコンテキストなしのJUnit 5とMockito #
Springの配線をテスト対象にしない場合は、MockitoExtensionを使用します。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class GreetingControllerMockitoTest {
@Mock
GreetingService service;
@InjectMocks
GreetingController controller;
@Test
void delegatesToTheService() {
when(service.greet("Ada"))
.thenReturn(new Greeting("Welcome, Ada"));
assertEquals(
new Greeting("Welcome, Ada"),
controller.get("Ada"));
verify(service).greet("Ada");
}
}このテストはSpringコンテキストを読み込みません。@InjectMocksはコントローラーのコンストラクターを呼び出し、@Mockは通常のMockitoオブジェクトを作成します。
SpringテストコンテキストでのMockito #
通常の@Mockフィールドは、SpringのApplicationContext内のBeanを置き換えません。スライステストまたは完全なコンテキストテストでBeanのオーバーライドが必要な場合は、@MockitoBeanを使用します。
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(GreetingController.class)
class GreetingControllerMockitoWebTest {
@Autowired
MockMvc mvc;
@MockitoBean
GreetingService service;
@Test
void rendersTheMockedServiceResult() throws Exception {
when(service.greet("Ada"))
.thenReturn(new Greeting("Welcome, Ada"));
mvc.perform(get("/greetings/Ada"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Welcome, Ada"));
verify(service).greet("Ada");
}
}Spring FrameworkのTestContextサポートはモックを作成し、コンテキストをリフレッシュする前に一致するBeanを置き換えます。Spring BootのテストアノテーションはすでにSpringExtensionを登録しているため、MockitoExtensionを追加してもコンテキストBeanの置き換えには役立ちません。
選択した実際の振る舞いを維持する必要がある場合は、@MockitoSpyBeanで既存のコンテキストBeanをラップします。Spring Bootの古い@MockBeanおよび@SpyBeanアノテーションは、Boot 3.4で非推奨となり、Spring Frameworkの@MockitoBeanおよび@MockitoSpyBeanが推奨されるようになりました。Boot 3.3以前のプロジェクトでは、古いアノテーションを使用する必要があります。
リモートクライアント、メッセージパブリッシャー、クロック、リポジトリなどのアーキテクチャ上の境界にBeanオーバーライドを適用します。完全なコンテキストテストで多くの内部Beanを置き換えると、@SpringBootTestで検証するために選択した配線が失われます。その場合は、通常の単体テストまたはMVCスライスのほうがスコープを正確に表現します。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)