REST Assured のセットアップ

// build.gradle
dependencies {
    testImplementation 'io.rest-assured:rest-assured:5.4.0'
    testImplementation 'io.rest-assured:json-path:5.4.0'
    testImplementation 'io.rest-assured:json-schema-validator:5.4.0'
    // Spring MVC テスト統合(Spring Boot プロジェクトの場合)
    testImplementation 'io.rest-assured:spring-mock-mvc:5.4.0'
}

// Java 9+ では以下の import を static に設定しておくと記述が楽になる
// import static io.restassured.RestAssured.*;
// import static io.restassured.matcher.RestAssuredMatchers.*;
// import static org.hamcrest.Matchers.*;

given / when / then 構文の基本

REST Assured は BDD スタイルの given / when / then で記述します。

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserApiTest {

    @BeforeAll
    static void setup() {
        RestAssured.baseURI  = "http://localhost";
        RestAssured.port     = 8080;
        RestAssured.basePath = "/api/v1";
    }

    @Test
    @Order(1)
    @DisplayName("ユーザー一覧を取得 — 200 OK でリストが返る")
    void getUsers_returns200WithList() {
        given()
            .header("Accept", "application/json")
        .when()
            .get("/users")
        .then()
            .statusCode(200)
            .contentType(ContentType.JSON)
            .body("size()", greaterThan(0))
            .body("[0].id",    notNullValue())
            .body("[0].name",  notNullValue());
    }

    @Test
    @Order(2)
    @DisplayName("ユーザーを新規作成 — 201 Created で ID が返る")
    void createUser_returns201WithId() {
        String requestBody = """
            {
                "name": "山田太郎",
                "email": "taro@example.com"
            }
            """;

        given()
            .contentType(ContentType.JSON)
            .body(requestBody)
        .when()
            .post("/users")
        .then()
            .statusCode(201)
            .body("id",    notNullValue())
            .body("name",  equalTo("山田太郎"))
            .body("email", equalTo("taro@example.com"));
    }

    @Test
    @DisplayName("存在しない ID は 404 を返す")
    void getUser_notFound_returns404() {
        given()
        .when()
            .get("/users/99999")
        .then()
            .statusCode(404)
            .body("errorCode", equalTo("USER_NOT_FOUND"));
    }
}

JsonPath での値抽出と検証

// レスポンスから値を抽出して JUnit アサーションで検証
import io.restassured.response.Response;
import static org.assertj.core.api.Assertions.*;

@Test
void getOrder_extractsAndValidatesItems() {
    Response response =
        given()
        .when()
            .get("/orders/ORD-001")
        .then()
            .statusCode(200)
            .extract().response();

    // JsonPath で個別フィールドを取得
    String  status     = response.jsonPath().getString("status");
    int     itemCount  = response.jsonPath().getInt("items.size()");
    String  firstItem  = response.jsonPath().getString("items[0].name");
    double  totalPrice = response.jsonPath().getDouble("totalPrice");

    assertThat(status).isEqualTo("CONFIRMED");
    assertThat(itemCount).isGreaterThan(0);
    assertThat(totalPrice).isGreaterThan(0.0);

    // ネストした値の検証
    // "items[0].quantity" のように . 記法でネスト可能
    List<String> itemNames = response.jsonPath().getList("items.name");
    assertThat(itemNames).contains("商品A");
}

認証(Bearer Token・Basic Auth)

// ── Bearer Token ──
given()
    .header("Authorization", "Bearer " + accessToken)
.when()
    .get("/users/me")
.then()
    .statusCode(200);

// ── Basic Auth ──
given()
    .auth().basic("admin", "password")
.when()
    .get("/admin/dashboard")
.then()
    .statusCode(200);

// ── OAuth2 トークン取得 → 使用の流れ ──
String token =
    given()
        .contentType("application/x-www-form-urlencoded")
        .formParam("grant_type",    "client_credentials")
        .formParam("client_id",     "my-client")
        .formParam("client_secret", "my-secret")
    .when()
        .post("http://auth.example.com/token")
    .then()
        .statusCode(200)
        .extract().jsonPath().getString("access_token");

// 取得したトークンで API を呼ぶ
given()
    .auth().oauth2(token)
.when()
    .get("/protected-resource")
.then()
    .statusCode(200);

Testcontainers のセットアップ

Testcontainers は Docker を使って本物の DB・メッセージブローカー・各種サービスをテスト中に起動できるライブラリです。

// build.gradle
dependencies {
    testImplementation 'org.testcontainers:testcontainers:1.19.8'
    testImplementation 'org.testcontainers:junit-jupiter:1.19.8'
    testImplementation 'org.testcontainers:postgresql:1.19.8'   // PostgreSQL 用
    testImplementation 'org.testcontainers:mysql:1.19.8'        // MySQL 用
}

// 前提条件: テスト実行環境に Docker がインストールされていること
// GitHub Actions では ubuntu-latest で Docker が使用可能

PostgreSQL コンテナを使った結合テスト

import org.junit.jupiter.api.*;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.*;
import java.sql.*;
import static org.assertj.core.api.Assertions.*;

@Testcontainers   // JUnit 5 拡張を有効化
class UserRepositoryIntegrationTest {

    // @Container アノテーションで自動起動・停止
    // static にするとクラス内のすべてのテストで共有(起動1回)
    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpass")
            .withInitScript("sql/schema.sql");  // 初期スキーマを流す

    UserRepository repository;

    @BeforeEach
    void setUp() {
        // コンテナの接続情報から DataSource を構成
        String url      = postgres.getJdbcUrl();
        String username = postgres.getUsername();
        String password = postgres.getPassword();

        var dataSource = createDataSource(url, username, password);
        repository = new UserRepository(dataSource);
    }

    @Test
    @DisplayName("ユーザーを保存し、ID で取得できる")
    void save_and_findById() {
        // ── Arrange ──
        User user = new User(null, "山田太郎", "taro@example.com");

        // ── Act ──
        User saved = repository.save(user);
        User found = repository.findById(saved.getId()).orElseThrow();

        // ── Assert ──
        assertThat(found.getName()).isEqualTo("山田太郎");
        assertThat(found.getEmail()).isEqualTo("taro@example.com");
    }

    @Test
    @DisplayName("存在しない ID は Optional.empty() を返す")
    void findById_notFound_returnsEmpty() {
        assertThat(repository.findById(99999L)).isEmpty();
    }
}

Spring Boot + Testcontainers の統合

Spring Boot 3.1 以降では @ServiceConnection で接続情報の自動設定が可能です。

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserControllerIntegrationTest {

    @Container
    @ServiceConnection   // Spring Boot が自動で DataSource を設定
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @LocalServerPort
    int port;

    @BeforeEach
    void setup() {
        RestAssured.port = port;
    }

    @Test
    @DisplayName("POST /users → DB に保存されて 201 が返る")
    void createUser_persistedToDatabase_returns201() {
        given()
            .contentType(ContentType.JSON)
            .body("""
                { "name": "テストユーザー", "email": "test@example.com" }
                """)
        .when()
            .post("/api/v1/users")
        .then()
            .statusCode(201)
            .body("id", notNullValue());
    }
}

Testcontainers Cloud で CI の Docker 制約を回避

一部の CI 環境では Docker in Docker が使えない場合があります。Testcontainers Cloud を使うとコンテナをクラウド側で実行できます。

まとめ

  • REST Assured の given / when / then 構文で API テストを Java コードとして管理でき、バージョン管理・コードレビューが容易になる
  • JsonPath で複雑なレスポンスの特定フィールドを抽出し、JUnit アサーションと組み合わせて詳細に検証できる
  • Testcontainers は H2 などのインメモリ DB の代替として本物の Docker DB を使用し、本番環境との差異を排除できる
  • Spring Boot 3.1+ の @ServiceConnection で設定コードを最小化できる
  • GitHub Actions(ubuntu-latest)では Docker が標準で使えるため、CI への組み込みが容易