2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Controller の単体テストを整理する(Spring Boot)

Last updated at Posted at 2024-11-25

はじめに

前回、SpringBoot + MyBatisの整理をしましたが、今回はControllerの単体テストを整理します。

Controllerからは、Serviceが呼び出されますが、そのServiceをモック化してテストをしていきます。
私は、JMockitを長く使ってましたが、Spring Boot Testでは、Mockito が使われるようなので、今回はSpringに合わせて実装し見ます。

図にするとこんな感じです。
image.png

環境

Windows 11
Eclipse
Java21
Gradle
Spring Boot 3.4.0
Spring Boot Test 3.4.0
Lombok
Mockito 5.14.2

単体テストの方針

以下のような方針でテストの実行方法を検討することにします。
ここでは具体的なテストケース、検証内容には触れません

・モックを利用する
・メソッドの呼び出し回数、引数をチェックする
・実行結果及び応答値を検証する

やってみる

テスト対象のクラス作る

Entityクラス

ExampleEntity.java
import lombok.Data;

@Data
public class ExampleEntity {

	long id;
	String message;
}

Serviceクラス

ExampleService.java
import org.springframework.stereotype.Service;

import sample.entity.ExampleEntity;

@Service
public class ExampleService {

	public ExampleEntity getExampleById(Long id) {
		ExampleEntity entity = new ExampleEntity();
		entity.setId(id);
		entity.setMessage("Example " + id);
		return entity;
	}
}

Controllerクラス

ExampleController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import sample.entity.ExampleEntity;
import sample.service.ExampleService;

@RestController
@RequestMapping("/api/example")
public class ExampleController {

	@Autowired
	private ExampleService exampleService;

	@GetMapping("/{id}")
	public ResponseEntity<ExampleEntity> getExample(@PathVariable Long id) {
		ExampleEntity response = exampleService.getExampleById(id);
		return ResponseEntity.ok(response);
	}
}

テストクラスを作る

ExampleContoller.java
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import sample.entity.ExampleEntity;
import sample.service.ExampleService;

// WebMvcTestアノテーションで、ExampleControllerのみをテスト対象に限定してWeb層のテストを設定。
@WebMvcTest(controllers = ExampleController.class)
class ExampleControllerTest {

    // MockMvcをDIで取得し、HTTPリクエスト/レスポンスのテストを行う。
	@Autowired
	private MockMvc mockMvc;

    // ExampleServiceのモックを作成してDI。モックの動作を設定してテストに利用。
	@MockitoBean
	private ExampleService exampleService;

    
	@Test
	void getExample_shouldReturnExampleResponse() throws Exception {

		// モックの設定
		Long testId = 1L;
		ExampleEntity mockResponse = new ExampleEntity();
		mockResponse.setId(testId);
		mockResponse.setMessage("Mocked Example " + testId);
  
        // モックされたexampleServiceのgetExampleByIdメソッドが呼ばれたとき、mockResponseを返すよう設定。
		when(exampleService.getExampleById(testId)).thenReturn(mockResponse);

		// テスト実行
		mockMvc.perform(get("/api/example/{id}", testId)
				.contentType(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())            // HTTPステータスコード200(OK)を期待。
				.andExpect(jsonPath("$.id").value(1))  // JSONの"id"フィールドを検証
				.andExpect(jsonPath("$.message").value("Mocked Example 1")); // レスポンスJSONの"message"フィールドが期待通りであることを検証。

		// Serviceが1回だけ呼ばれたことを検証
		verify(exampleService, times(1)).getExampleById(testId);

		// Service2回以上呼ばれたことを検証
		//verify(exampleService, atLeast(2)).getExampleById(testId);
		
		//一度も呼び出されていない場合の検証:
		//verifyNoInteractions(exampleService);
		
        // サービスの特定メソッドが呼び出されていないことを検証
        //verify(exampleService, times(0)).getExampleById(any());

	}
}

andExceptで使用できるメソッドの主な例を記載しておきます。

カテゴリー メソッド 説明
レスポンスステータス status().isOk() ステータスコード200(OK)を検証
status().isCreated() ステータスコード201(Created)を検証
status().isNotFound() ステータスコード404(Not Found)を検証
status().isBadRequest() ステータスコード400(Bad Request)を検証
レスポンスヘッダー header().exists("Header-Name") 指定したヘッダーが存在することを検証
header().string("Header-Name", "Header-Value") 指定したヘッダーの値が一致することを検証
header().doesNotExist("Header-Name") 指定したヘッダーが存在しないことを検証
レスポンス本文 content().string("Expected Response Body") レスポンス本文が完全一致することを検証
content().json("{\"key\":\"value\"}") レスポンスが指定したJSONと一致することを検証
content().xml("<tag>value</tag>") レスポンスが指定したXMLと一致することを検証
content().contentType(MediaType.APPLICATION_JSON) Content-TypeがJSONであることを検証
content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON) Content-TypeがJSONと互換性があることを検証
content().bytes(byte[]) レスポンスのバイナリデータが一致することを検証
JSONフィールド jsonPath("$.id").value(1) JSONの"id"フィールドが1であることを検証
jsonPath("$.name").exists() JSONの"name"フィールドが存在することを検証
jsonPath("$.details").isEmpty() JSONの"details"フィールドが空であることを検証
jsonPath("$.items").isArray() JSONの"items"フィールドが配列であることを検証
レスポンスCookie cookie().exists("SESSION") 特定のCookieが存在することを検証
cookie().value("SESSION", "expectedValue") 特定のCookieの値が一致することを検証
cookie().maxAge("SESSION", 3600) Cookieの有効期限が一致することを検証
その他の検証 request().attribute("attributeName", "expectedValue") リクエストの属性が特定の値であることを検証
flash().attribute("flashAttributeName", "expectedValue") Flashスコープの属性が特定の値であることを検証
redirectedUrl("/expected/url") リダイレクト先のURLが一致することを検証
redirectedUrlPattern("/expected/*") リダイレクトURLが指定したパターンに一致することを検証

実行する

JUnitなので、Eclipse上で実行してみてください。
以下のような実行結果が表示されればOKです。

image.png

さいごに

今回は、Contollerクラスを対象にしました。次回はServiceクラスを予定していますが、
基本的にはMock化するだけなので実施方法は同じです(呼び出し部分が若干異なる)

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?