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 のいずれかと、使用する言語を選択します。このガイドは、Java を選択したことを前提としています。

  3. 依存関係をクリックして、Spring Web を選択します。

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

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

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

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

Spring アプリケーション用の新しいコントローラーを作成します。次のリスト(src/main/java/com/example/testingweb/HomeController.java から)は、その方法を示しています。

package com.example.testingweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HomeController {

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

}
前の例では、GET と PUTPOST などを指定していません。デフォルトでは、@RequestMapping はすべての HTTP 操作をマップします。@GetMapping または @RequestMapping(method=GET) を使用して、このマッピングを絞り込むことができます。

アプリケーションの実行

Spring Initializr は、アプリケーションクラス(main() メソッドを持つクラス)を作成します。このガイドでは、このクラスを変更する必要はありません。次のリスト(src/main/java/com/example/testingweb/TestingWebApplication.java から)は、Spring Initializr が作成したアプリケーションクラスを示しています。

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);
	}
}

@SpringBootApplication は、次のすべてを追加する便利なアノテーションです。

  • @Configuration: アプリケーションコンテキストの Bean 定義のソースとしてクラスにタグを付けます。

  • @EnableAutoConfiguration: クラスパス設定、他の Bean、さまざまなプロパティ設定に基づいて Bean の追加を開始するよう Spring Boot に指示します。

  • @EnableWebMvc: アプリケーションを Web アプリケーションとしてフラグを立て、DispatcherServlet のセットアップなどの主要な動作をアクティブにします。Spring Boot は、クラスパスで spring-webmvc を検出すると自動的に追加します。

  • @ComponentScan: アノテーション付き TestingWebApplication クラス (com.example.testingweb) が存在するパッケージ内の他のコンポーネント、構成、サービスを検索するように Spring に指示し、com.example.testingweb.HelloController を見つけさせます。

main() メソッドは、Spring Boot の SpringApplication.run() メソッドを使用してアプリケーションを起動します。XML の単一行がないことに気づきましたか? web.xml ファイルもありません。この Web アプリケーションは 100% 純粋な Java であり、接続機能やインフラストラクチャの構成に対処する必要はありませんでした。Spring Boot がすべてを処理します。

ロギング出力が表示されます。サービスは数秒以内に起動して実行されるはずです。

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

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

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

最初にできることは、アプリケーションコンテキストを開始できない場合に失敗する単純な健全性チェックテストを記述することです。次のリスト(src/test/java/com/example/testingweb/TestingWebApplicationTest.java から)は、その方法を示しています。

package com.example.testingweb;

import org.junit.jupiter.api.Test;

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

@SpringBootTest
class TestingWebApplicationTests {

	@Test
	void contextLoads() {
	}

}

@SpringBootTest アノテーションは、Spring Boot にメイン構成クラス(たとえば @SpringBootApplication を持つもの)を探し、それを使用して Spring アプリケーションコンテキストを開始するように指示します。このテストは、IDE またはコマンドライン(./mvnw test または ./gradlew test を実行することで)で実行でき、パスするはずです。コンテキストがコントローラーを作成していることを確信させるために、次の例(src/test/java/com/example/testingweb/SmokeTest.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();
	}
}

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

Spring Test サポートの優れた機能は、アプリケーションコンテキストがテスト間でキャッシュされることです。そうすれば、テストケースに複数のメソッドがある場合、同じ構成の複数のテストケースがある場合、アプリケーションを 1 回だけ起動するコストが発生します。@DirtiesContext (Javadoc) アノテーションを使用してキャッシュを制御できます。

健全性チェックがあると便利ですが、アプリケーションの動作をアサートするテストも作成する必要があります。これを行うには、アプリケーションを起動して接続をリッスンし(運用環境で行うように)、HTTP リクエストを送信してレスポンスをアサートします。次のリスト(src/test/java/com/example/testingweb/HttpRequestTest.java から)は、その方法を示しています。

package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.web.server.LocalServerPort;

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

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

	@LocalServerPort
	private int port;

	@Autowired
	private TestRestTemplate restTemplate;

	@Test
	void greetingShouldReturnDefaultMessage() throws Exception {
		assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/",
				String.class)).contains("Hello, World");
	}
}

webEnvironment=RANDOM_PORT を使用してランダムポートでサーバーを起動することに注意してください(テスト環境での競合を回避するのに便利です)および @LocalServerPort でポートを挿入します。また、Spring Boot が自動的に TestRestTemplate を提供していることに注意してください。@Autowired を追加するだけです。

もう 1 つの便利なアプローチは、サーバーをまったく起動せず、Spring が受信 HTTP リクエストを処理してコントローラーに渡す、そのレイヤーのみをテストすることです。こうすることで、ほぼすべてのフルスタックが使用され、実際の HTTP リクエストを処理している場合とまったく同じ方法でコードが呼び出されますが、サーバーを起動するコストはかかりません。これを行うには、Spring の MockMvc を使用し、テストケースの @AutoConfigureMockMvc アノテーションを使用してそれを挿入するように依頼します。次のリスト ( src/test/java/com/example/testingweb/TestingWebApplicationTest.java から) は、その方法を示しています。

package com.example.testingweb;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class TestingWebApplicationTest {

	@Autowired
	private MockMvc mockMvc;

	@Test
	void shouldReturnDefaultMessage() throws Exception {
		this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, World")));
	}
}

このテストでは、完全な Spring アプリケーションコンテキストが開始されますが、サーバーはありません。次のリスト(src/test/java/com/example/testingweb/WebLayerTest.java から)が示すように、@WebMvcTest を使用して、テストを Web レイヤーのみに絞り込むことができます。

@WebMvcTest
include::complete/src/test/java/com/example/testingweb/WebLayerTest.java

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

これまでのところ、HomeController は単純であり、依存関係はありません。あいさつを保存するための追加コンポーネントを導入することで、より現実的にすることができます(おそらく新しいコントローラーに)。次の例(src/main/java/com/example/testingweb/GreetingController.java から)は、その方法を示しています。

package com.example.testingweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
public class GreetingController {

	private final GreetingService service;

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

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

}

次に、次のリスト(src/main/java/com/example/testingweb/GreetingService.java から)が示すように、グリーティングサービスを作成します。

package com.example.testingweb;

import org.springframework.stereotype.Service;

@Service
public class GreetingService {
	public String greet() {
		return "Hello, World";
	}
}

Spring は、コンストラクターのシグネチャーのために、コントローラーにサービスの依存関係を自動的に挿入します。次のリスト(src/test/java/com/example/testingweb/WebMockTest.java から)は、このコントローラーを @WebMvcTest でテストする方法を示しています。

package com.example.testingweb;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
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.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
class WebMockTest {

	@Autowired
	private MockMvc mockMvc;

	@MockBean
	private GreetingService service;

	@Test
	void greetingShouldReturnMessageFromService() throws Exception {
		when(service.greet()).thenReturn("Hello, Mock");
		this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, Mock")));
	}
}

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

要約

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

関連事項

次のガイドも役立ちます。

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

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の Attribution、NoDerivatives creative commons ライセンス (英語) でリリースされています。

コードを入手する