概要
単体テストだけでは拾えない不具合がある。コンポーネント同士の結合部分、HTTP リクエストからレスポンスまでの一連の流れ、データベースとの整合性――こうした層をカバーするのが統合テストと E2E テストの役割です。一方で「テストが遅い」「環境依存で CI が落ちる」という声も現場では少なくありません。この記事では、テストピラミッドの考え方を土台に、Spring Boot の @SpringBootTest と TestRestTemplate を使い、REST API の CRUD エンドポイントを実際に起動して検証する統合テストの書き方を示します。テスト用のポート設定、テストデータの初期化、レスポンスの検証まで、実務で迷いやすいポイントをコード付きで押さえます。
使いどころ
REST API の CRUD に対してリクエスト送信からレスポンスまで一気通貫で検証する統合テストを整備する
マイクロサービス間の API 呼び出しが正しいステータスコードを返すことを TestRestTemplate で確認する
既存の手動テスト手順を自動化し、CI パイプラインに組み込む
認証フィルタや例外ハンドラを含む HTTP の振る舞いを、アプリ起動込みで確認する
コントローラ、サービス、DB の結線が意図どおり動くかをデプロイ前にまとめて確認する
コード例
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.annotation.DirtiesContext;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class EmployeeApiIntegrationTest {
@Autowired TestRestTemplate restTemplate;
record EmployeeResponse(Long id, String name, String department) {}
record CreateEmployeeRequest(String name, String department) {}
@Test @DisplayName("登録して取得できること")
void createAndGet() {
var req = new CreateEmployeeRequest("田中太郎", "開発部");
var createRes = restTemplate.postForEntity("/api/employees", req, EmployeeResponse.class);
assertEquals(HttpStatus.CREATED, createRes.getStatusCode());
var getRes = restTemplate.getForEntity("/api/employees/" + createRes.getBody().id(), EmployeeResponse.class);
assertEquals(HttpStatus.OK, getRes.getStatusCode());
assertEquals("田中太郎", getRes.getBody().name());
}
@Test @DisplayName("削除できること")
void delete() {
var req = new CreateEmployeeRequest("鈴木一郎", "総務部");
var created = restTemplate.postForEntity("/api/employees", req, EmployeeResponse.class);
restTemplate.delete("/api/employees/" + created.getBody().id());
var getRes = restTemplate.getForEntity("/api/employees/" + created.getBody().id(), String.class);
assertEquals(HttpStatus.NOT_FOUND, getRes.getStatusCode());
}
@Test @DisplayName("存在しない ID は 404")
void notFound() {
var res = restTemplate.getForEntity("/api/employees/99999", String.class);
assertEquals(HttpStatus.NOT_FOUND, res.getStatusCode());
}
}Version Coverage
record でレスポンス DTO を簡潔に定義できる。Spring Boot 3.x は Java 17 以上を要求する。
// Java 17: record でレスポンス DTO を定義
record EmployeeResponse(Long id, String name, String department) {}
ResponseEntity<EmployeeResponse> response =
restTemplate.getForEntity("/api/employees/1", EmployeeResponse.class);
assertEquals("田中太郎", response.getBody().name());Library Comparison
注意点
@SpringBootTest はコンテキスト全体を起動するため実行時間が長い。統合テストは必要な範囲に絞る
webEnvironment = RANDOM_PORT を指定しないとポート競合で CI が失敗する
テスト間でDB状態を共有するとフレイキーテストの原因になる。@BeforeEach でデータを初期化する
すべてのテストで @SpringBootTest を使うとテストスイートの実行時間が膨れ上がる
TestRestTemplate はリダイレクトを自動で追わない設定がデフォルト
時刻や外部 API 応答などの不安定要素をそのまま含めると E2E テストが壊れやすい。固定化やスタブ化できる境界を見極める
単体テストで十分確認できる分岐まで統合テストへ持ち込むと、失敗時の原因切り分けが難しくなる
FAQ
コントローラ単体は @WebMvcTest、サービス層やDBを含む一気通貫テストは @SpringBootTest を使います。
Servlet ベースは TestRestTemplate、WebFlux ベースは WebTestClient が適しています。
CI では H2 や Testcontainers で再現性を確保し、本番同等の検証はステージングで行うのが現実的です。
主要な業務フローや障害時の復旧動線など、手戻りコストが高い経路を優先するのが現実的です。すべてを UI 経由で自動化すると保守コストが急増します。
まず @SpringBootTest の件数とスコープを見直します。次に DB 初期化や外部依存の起動コストを確認し、MockMvc や slice test に切り分けられる箇所を減らしていきます。