はじめに
前回、SpringBoot + MyBatisの整理をしましたが、今回はControllerの単体テストを整理します。
Controllerからは、Serviceが呼び出されますが、そのServiceをモック化してテストをしていきます。
私は、JMockitを長く使ってましたが、Spring Boot Testでは、Mockito が使われるようなので、今回はSpringに合わせて実装し見ます。
環境
Windows 11
Eclipse
Java21
Gradle
Spring Boot 3.4.0
Spring Boot Test 3.4.0
Lombok
Mockito 5.14.2
単体テストの方針
以下のような方針でテストの実行方法を検討することにします。
ここでは具体的なテストケース、検証内容には触れません
・モックを利用する
・メソッドの呼び出し回数、引数をチェックする
・実行結果及び応答値を検証する
やってみる
テスト対象のクラス作る
Entityクラス
import lombok.Data;
@Data
public class ExampleEntity {
long id;
String message;
}
Serviceクラス
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クラス
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);
}
}
テストクラスを作る
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です。
さいごに
今回は、Contollerクラスを対象にしました。次回はServiceクラスを予定していますが、
基本的にはMock化するだけなので実施方法は同じです(呼び出し部分が若干異なる)