MockMvc と @MockBean で Web レイヤーテスト

このガイドでは、Spring アプリケーションを作成し、それを JUnit でテストするプロセスを説明します。

構築するもの

単純な Spring アプリケーションを構築し、JUnit でテストします。アプリケーション内の個々のクラスの単体テストを記述して実行する方法をすでに知っている可能性があるため、このガイドでは、Spring Test および Spring Boot 機能を使用して Spring とコード間の相互作用をテストすることに集中します。アプリケーションコンテキストが正常に読み込まれるという簡単なテストから始めて、Spring の MockMvc を使用して Web レイヤーのみをテストし続けます。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Spring Initializr から開始に進みます。

基本スキップするには、次の手順を実行します。

完了したときは、gs-testing-web/complete のコードに対して結果を確認できます。

Spring Initializr から開始

IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。

プロジェクトを手動で初期化するには:

  1. IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。

  2. Gradle または Maven のいずれかと、使用する言語を選択してください。

  3. 依存関係をクリックし、Spring WebHTTP クライアントを選択します。

  4. 生成をクリックします。

  5. 結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。

EclipseIntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。
プロジェクトを GitHub からフォークして、IDE または他のエディターで開くこともできます。

シンプルなアプリケーションを作成する

Spring アプリケーション用の新しいコントローラーを作成します。以下のリストにその方法を示します。

Java
package com.example.testingweb;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {

	@GetMapping("/")
	public String greeting() {
		return "Hello, World";
	}

}
Kotlin
package com.example.testingweb

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HomeController {

    @GetMapping("/")
    fun greeting(): String = "Hello, World"
}

アプリケーションの実行

Spring Initializr は、アプリケーションクラス(main() メソッドを持つクラス)を作成します。このガイドでは、このクラスを変更する必要はありません。以下のリストは、Spring Initializr が作成したアプリケーションクラスを示しています。

Java
package com.example.testingweb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestingWebApplication {

	public static void main(String[] args) {
		SpringApplication.run(TestingWebApplication.class, args);
	}
}
Kotlin
package com.example.testingweb

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class TestingWebApplication

fun main(args: Array<String>) {
    runApplication<TestingWebApplication>(*args)
}

このアプリケーションクラスは、IDE から直接実行することも、Gradle (./gradlew :bootRun) または Maven (./mvwn spring-boot:run) を使用してコマンドラインから実行することもできます。

アプリケーションが実行されたため、テストできます。http://localhost:8080 でホームページをロードできます。ただし、変更を加えたときにアプリケーションが機能するという自信を高めるために、テストを自動化する必要があります。

アプリケーションをテストする

Spring Boot は、アプリケーションのテストを計画していると想定しているため、ビルドファイル(build.gradle または pom.xml)に必要な依存関係を追加します。

まず最初にできることは、アプリケーションコンテキストが起動できない場合に失敗するような、シンプルなサニティチェックテストを作成することです。以下にその方法を示します。

Java
package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class TestingWebApplicationTests {

	@Test
	void contextLoads() {
	}

}
Kotlin
package com.example.testingweb

import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class TestingWebApplicationTests {

    @Test
    fun contextLoads() {
    }
}

@SpringBootTest アノテーションは、Spring Boot にメイン構成クラス(たとえば @SpringBootApplication を含むクラス)を探し、それを使って Spring アプリケーションコンテキストを起動するように指示します。このテストは IDE またはコマンドライン(./mvnw test または ./gradlew check を実行)で実行でき、パスするはずです。コンテキストがコントローラーを作成していることを確認するには、次の例のようにアサーションを追加できます。

Java
package com.example.testingweb;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SmokeTest {

	@Autowired
	private HomeController controller;

	@Test
	void contextLoads() throws Exception {
		assertThat(controller).isNotNull();
	}
}
Kotlin
package com.example.testingweb

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class SmokeTest(@Autowired private val controller: HomeController) {

    @Test
    fun contextLoads() {
        assertThat(controller).isNotNull()
    }
}

Spring は @Autowired アノテーションを解釈し、テストメソッドが実行される前にコントローラーが挿入されます。AssertJ (英語) assertThat() およびその他のメソッドを提供)を使用して、テストアサーションを表現します。

健全性チェックは有効ですが、アプリケーションの動作をアサートするテストもいくつか記述する必要があります。そのためには、アプリケーションを起動し、(本番環境と同様に)接続をリッスンしてから、HTTP リクエストを送信し、レスポンスをアサートします。以下のリストは、その方法を示しています。

Java
package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.web.servlet.client.RestTestClient;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class HttpRequestTest {

	@LocalServerPort
	private int port;

	@Autowired
	private RestTestClient restTestClient;

	@Test
	void greetingShouldReturnDefaultMessage() {
		restTestClient.get()
				.uri("http://localhost:%d/".formatted(port))
				.exchange()
				.expectBody(String.class)
				.isEqualTo("Hello, World");
	}
}
Kotlin
package com.example.testingweb

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class HttpRequestTest(@Autowired private val restTestClient: RestTestClient) {

    @LocalServerPort
    private var port: Int = 0

    @Test
    fun greetingShouldReturnDefaultMessage() {
        // Import Kotlin .expectBody() extension that allows using reified type parameters
        restTestClient.get()
            .uri("http://localhost:$port/")
            .exchange()
            .expectBody<String>()
            .isEqualTo("Hello, World")
    }

}

webEnvironment=RANDOM_PORT を使用してサーバーをランダムポートで起動し(テスト環境での競合を回避できます)、@LocalServerPort を使用してポート番号を注入していることにご注意ください。また、@AutoConfigureRestTestClient で必要であることを指定したため、Spring Boot によって RestTestClient が自動的に提供されていることにもご注目ください。あとは、@Autowired をフィールドに追加するだけです。

もう 1 つの便利なアプローチは、サーバーを全く起動せず、そのレイヤー、つまり Spring が受信した HTTP リクエストを処理し、それをコントローラーに渡すレイヤーのみをテストすることです。こうすることで、フルスタックのほぼすべてが使用され、コードは実際の HTTP リクエストを処理する場合と全く同じように呼び出されますが、サーバーの起動コストはかかりません。これを行うには、前回の RestTestClient を使用したテストを再利用しますが、今回は @SpringBootTest をデフォルト(モックサーバー環境を起動する)のままにします。以下のリストは、その方法を示しています。

Java
package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.client.RestTestClient;

@SpringBootTest
@AutoConfigureRestTestClient
class TestingWebApplicationTest {

	@Autowired
	private RestTestClient restTestClient;

	@Test
	void greetingShouldReturnDefaultMessage() {
		restTestClient.get().uri("/")
				.exchange()
				.expectBody(String.class)
				.isEqualTo("Hello, World");
	}
}
Kotlin
package com.example.testingweb

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody

@SpringBootTest
@AutoConfigureRestTestClient
class TestingWebApplicationTest(@Autowired private val restTestClient: RestTestClient) {

    @Test
    fun greetingShouldReturnDefaultMessage() {
        // Import Kotlin .expectBody() extension that allows using reified type parameters
        restTestClient.get().uri("/")
            .exchange()
            .expectBody<String>()
            .isEqualTo("Hello, World")
    }

}

このテストでは、Spring アプリケーションコンテキスト全体が起動されますが、サーバーは起動されません。@WebMvcTest を使用することで、以下のリストに示すように、テストを Web レイヤーのみに絞り込むことができます。

Java
package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.web.servlet.client.RestTestClient;

@WebMvcTest(HomeController.class)
@AutoConfigureRestTestClient
class WebLayerTest {

	@Autowired
	private RestTestClient restTestClient;

	@Test
	void greetingShouldReturnDefaultMessage() {
		restTestClient.get().uri("/")
				.exchange()
				.expectBody(String.class)
				.isEqualTo("Hello, World");
	}
}
Kotlin
package com.example.testingweb

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody

@WebMvcTest(HomeController::class)
@AutoConfigureRestTestClient
class WebLayerTest(@Autowired private val restTestClient: RestTestClient) {

    @Test
    fun greetingShouldReturnDefaultMessage() {
        // Import Kotlin .expectBody() extension that allows using reified type parameters
        restTestClient.get()
            .uri("/")
            .exchange()
            .expectBody<String>()
            .isEqualTo("Hello, World")
    }

}

テストアサーションは、前のケースと同じです。ただし、このテストでは、Spring Boot はコンテキスト全体ではなく Web レイヤーのみをインスタンス化します。複数のコントローラーを備えたアプリケーションでは、たとえば @WebMvcTest(HomeController.class) を使用して、1 つのみのインスタンス化を要求することもできます。

今のところ、HomeController はシンプルで依存関係もありません。挨拶文を保存するための追加コンポーネント(おそらく新しいコントローラー)を導入することで、より現実的なものにすることができます。次の例は、その方法を示しています。

Java
package com.example.testingweb;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class GreetingController {

	private final GreetingService service;

	public GreetingController(GreetingService service) {
		this.service = service;
	}

	@GetMapping("/greeting")
	public String greeting() {
		return service.greet();
	}

}
Kotlin
package com.example.testingweb

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class GreetingController(private val service: GreetingService) {

    @GetMapping("/greeting")
    fun greeting(): String = service.greet()
}

次に、次のように、グリーティングサービスを作成します。

Java
package com.example.testingweb;

import org.springframework.stereotype.Service;

@Service
public class GreetingService {
	public String greet() {
		return "Hello, World";
	}
}
Kotlin
package com.example.testingweb

import org.springframework.stereotype.Service

@Service
class GreetingService {
    fun greet(): String = "Hello, World"
}

Spring は、コンストラクターのシグネチャーに基づいて、サービス依存関係をコントローラーに自動的に注入します。以下のリストは、このコントローラーを @WebMvcTest でテストする方法を示しています。

Java
package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.client.RestTestClient;

import static org.mockito.Mockito.when;

@WebMvcTest(GreetingController.class)
@AutoConfigureRestTestClient
class WebMockTest {

	@Autowired
	private RestTestClient restTestClient;

	@MockitoBean
	private GreetingService service;

	@Test
	void greetingShouldReturnMessageFromService() throws Exception {
		when(service.greet()).thenReturn("Hello, Mock");
		restTestClient.get().uri("/greeting")
				.exchange()
				.expectBody(String.class)
				.isEqualTo("Hello, Mock");
	}
}
Kotlin
package com.example.testingweb

import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody

@WebMvcTest(GreetingController::class)
@AutoConfigureRestTestClient
class WebMockTest(@Autowired private val restTestClient: RestTestClient) {

    @MockitoBean
    private lateinit var service: GreetingService

    @Test
    fun greetingShouldReturnMessageFromService() {
        `when`(service.greet()).thenReturn("Hello, Mock")
        // Import Kotlin .expectBody() extension that allows using reified type parameters
        restTestClient.get()
            .uri("/greeting")
            .exchange()
            .expectBody<String>()
            .isEqualTo("Hello, Mock")
    }
}

@MockitoBean を使用して GreetingService のモックを作成および注入し(そうしないと、アプリケーションコンテキストを開始できません)、Mockito を使用してその期待値を設定します。

要約

おめでとう! Spring アプリケーションを開発し、JUnit および Spring MockMvc でテストし、Spring Boot を使用して Web レイヤーを分離し、特別なアプリケーションコンテキストをロードしました。

関連事項

次のガイドも役立つかもしれません:

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の帰属表示、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。

コードを入手する