0
0

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クラス,入力チェックのテスト実装

0
Posted at

🕒 学習時間

12:00~15:00

🧑‍💻 実施した学習内容

@WebMvcTest

◾️Spring MVCのコントローラ層だけを切り出してテストするためのアノテーションです。サービス層やリポジトリ層は読み込まず、軽量かつ高速にHTTPレベルの振る舞いを検証できます。

◾️なぜ使うのか?
@SpringBootTest は全コンテキストを起動するため:
・起動が遅い(数秒〜数十秒)
・テストの責務が曖昧になる

@WebMvcTest は:
・起動が高速(数百ms〜1秒程度)
・Controllerの責務(HTTP入出力)に集中できる

◾️重要事項
①MockMvc が主役
・HTTPリクエストを擬似的に送るためのAPI
→ 実際のHTTP通信なしでControllerを検証

mockMvc.perform(get("/users/1"))

@MockBean は必須

@WebMvcTest はServiceをロードしないため:

@MockBean
private UserService userService;

上記を忘れると下記のエラーになる

No qualifying bean of type 'UserService'

✅ MockMvcについて

◾️MockMvc
・Spring MVCのコントローラをHTTPサーバを起動せずに擬似的に呼び出すテストツール用(Spring公式のクラス)
・HTTPリクエスト(GET/POSTなど)を疑似的に生成し、Controllerに直接渡す
・実際のTomcatなどは起動しない → 高速・軽量

mockMvc.perform(get("/users/1"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$.name").value("Taro"));

@MockBeanについて

◾️Springコンテナに登録されているBeanをMockitoのモックに差し替えるためのアノテーション

◾️なぜ必要か?

・Controllerは通常Serviceに依存している

Controller  Service  Repository

でもテストでは:
Serviceの中身は信用しない(別でテストすべき)
Controllerだけ検証したい

そこで @MockBean を使う

◾️本質
依存を切り離して「テスト対象だけ」を純粋に検証する

◾️注意

① MockMvc単体では不十分
MockMvcだけだと依存関係はそのまま動く
→ DBアクセスなどが発生する可能性

必ず @MockBean とセットで使うケースが多い

@MockBeanはSpringコンテナを書き換える
通常の @Mock(Mockito)とは違う
Spring管理のBeanに対して効く

③テストの粒度を間違えない

◾️失敗
・ControllerテストでServiceのロジックも検証しようとする

◾️正解
・Controller → HTTP・レスポンスだけ検証
・Service → ビジネスロジックを別テスト

@MockBean
private UserService userService;

when(userService.findById(1L)).thenReturn(new User("Taro"));

@WebMvcTestになぜ@Autowiredを付与するのか

◾️ 理由
@WebMvcTest@Autowiredを付ける理由は、「Springがテスト用に生成したBean(特にMockMvcや対象Controller)をDIコンテナから取得するため」です。
つまり、自分でnewするのではなく、Springのテストコンテナに管理させたオブジェクトを注入するために必須です。
◾️(例:料理)
@WebMvcTest@Autowiredを付けるのは、**「自分で料理を作るのではなく、完成した料理を厨房(Spring)から受け取るため」**です。
つまり、Springが用意した完成品(MockMvcやController)を受け取る“受け取り口”が@Autowiredです。

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc; // ← Springが生成したBeanを注入

    @MockBean
    private UserService userService; // ← 依存をMockに差し替え

}

◾️状況設定
あなた:ホールスタッフ(テストコード)
厨房:Springコンテナ
料理人:Spring Bootの自動設定
料理:ControllerやMockMvc

◾️正しい(@Autowiredあり)
・厨房で料理を作る。あなたはそれを受け取って提供する

1.SpringがMockMvcを作る
2.@Autowiredで受け取る
3.テストで使う

@Autowired
MockMvc mockMvc;

◾️間違い(@Autowiredなし)

・注文したのに料理が届かない
・mockMvcは null
・使うとエラー(NullPointerException)

MockMvc mockMvc;

◾️さらにダメ(自分でnew)

・厨房を無視して自分で料理しようとする
・調理器具(Spring設定)がない
・レシピ(DI・Filterなど)もない
・正しい料理にならない

MockMvc mockMvc = new MockMvc();

✅ performは何か

◾️結論

・performは、MockMvcで「HTTPリクエストを実際に実行する命令」するメソッドです。
・つまり、「テストの中で疑似的にWebアクセスを発生させる操作」です。

◾️根拠

【根拠】

・Springのテストフレームワーク(Spring Test / MockMvc)において、
MockMvc.performは以下の役割を持ちます:

・リクエスト(GET / POSTなど)を受け取る
・Spring MVCのDispatcherServletに渡す
・Controller → Service → Response の流れを実行
・結果(ステータス・レスポンス)を返す

◾️例:料理
・perform = 注文を厨房に出す
・Controller = 料理人
・Service = 調理工程
・Response = 完成した料理

perform → 注文する
Controller → 料理人が受ける
Service → 調理する
return → 料理が出てくる

・注文しないと料理は出てこない
・それが「performしないとテストは何も起きない」という意味です。

◾️具体例

1.perform(get("/users"))
→GETリクエストを送る(ここが実行トリガー)
2.andExpect(...)
→結果を検証する
3.メソッド意味
→status():HTTPステータスを検証
→isOk():200 OKであることを期待
→content():レスポンスの中身
→string("OK"):レスポンス本文が完全一致で "OK"を返す
→json("{"name":"taro"}"):レスポンスボディが指定したJSONと一致するか検証

mockMvc.perform(get("/users")) 
       .andExpect(status().isOk())
       .andExpect(content().string("OK"));
       .andExpect(content().json("{\"name\":\"taro\"}"))

◾️注意事項

①performしないと何も起きない

mockMvc.get("/users"); // ❌意味ない(存在しない)

②HTTPメソッドを必ず指定する

perform(get("/users"))
perform(post("/users"))
perform(put("/users/1"))
perform(delete("/users/1"))

③レスポンスは必ず検証するべき

.andExpect(status().isOk()); //書かないと「テストとして弱い」

◾️より実務的な検証(JSON形式)

・部分一致で柔軟
・可読性が高い

.andExpect(jsonPath("$.name").value("taro"))

✅ MockMvcResultBuildersとは

◾️結論
・MockMvcResultBuilders は、Spring Framework のテスト機能で使われる「HTTPレスポンスの期待結果(Expected)を構築するためのユーティリティクラス」です。
andExpect() と組み合わせて、「レスポンスがどうあるべきか」を定義する役割を持ちます。

◾️根拠
【根拠】
MockMvc テストは基本的に以下の3ステップで構成されます:

1.実行(リクエスト送信)
2.検証(レスポンス確認)
3.後処理

・このうち「検証」で使われるのが MockMvcResultMatchers と MockMvcResultBuilders です。

・MockMvcResultMatchers
→ レスポンスを検証する(status, contentなど)
・MockMvcResultBuilders
→ レスポンス情報(ModelAndViewなど)を構築するための補助

ただし実務では、以下のようにstatic importで間接的に使うケースがほとんどです:

mockMvc.perform(get("/users"))
       .andExpect(status().isOk())
       .andExpect(content().string("OK"));

◾️注意
・名前が似ているため混同しやすい:

MockMvcRequestBuilders  リクエスト作成performの中
MockMvcResultMatchers  検証andExpect
MockMvcResultBuilders  レスポンス構築補助内部寄り

✅ 入力チェックのテスト導入

Validator validator = Validation
    .buildDefaultValidatorFactory()
    .getValidator();

◾️用語解説
・Validator / Validationとは「入力データがルール(制約)を満たしているか検証する仕組み」です。

・Validation(バリデーション):検証という「概念・処理そのもの」
・Validator(バリデータ):実際に検証を実行する「オブジェクト(実装)」

◾️用語解説
・validation.builddefaultvalidatorfactory().getvalidator()とは
「Bean Validation(入力チェック)の実行エンジン(Validator)を取得するための標準的な初期化コード」です。
つまり、「バリデーションを実行するための本体を取得する処理」

✅ validator.validate

・オブジェクトの内容が定義されたルール(制約)に違反していないかをチェックするメソッドです。
・「オブジェクトが“正しい状態かどうか”を機械的に検査する関数」

◾️具体例

public class User {
    @NotNull
    private String name;

    @Min(18)
    private int age;

    // getter/setter
}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

User user = new User();
user.setName(null);
user.setAge(15);

Set<ConstraintViolation<User>> violations = validator.validate(user);

✅ violationsとは

・ルール・制約・仕様に違反している状態や内容の集合(リスト)」を指す用語です。
・特にJavaでは、バリデーション(入力チェック)で見つかったエラー一覧として使われます。

Set<ConstraintViolation<User>> violations = validator.validate(user);

ここでは:
・ConstraintViolation = 「1つの違反(エラー)」
・Set = 「違反の一覧」

◾️結果
violations = バリデーションに失敗した項目の集合

✅ assertThatとは

・assertThatとは、「実際の値(Actual)が期待値(Expected)どおりかを検証するためのアサーション(検証メソッド)」です。
特に、読みやすく・拡張性の高いテストを書くための構文として使われます。

◾️基本構文

assertThat(実際の値).条件(期待値);

◾️具体例

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

@Test
void test() {
    int result = 2 + 3;

    assertThat(result).isEqualTo(5);
}

・result(実際の値)が、5(期待値)と等しいかチェック

◾️なぜ使うのか

assertEquals(5, result);

従来のJUnitは:
・引数の順番が分かりにくい
・読みづらい

assertThat(result).isEqualTo(5);

assertThatの場合:
・英語の文章のように読める
・拡張しやすい
・条件を連鎖できる

◾️代表例

assertThat(name).isEqualTo("Taro");     // 等しい
assertThat(list).isEmpty();             // 空か
assertThat(age).isGreaterThan(20);      // より大きい
assertThat(text).contains("Hello");     // 含む

◾️注意

  1. ライブラリによって違う
    ・JUnit単体 → assertEquals
    ・AssertJ → assertThat(推奨)
    ・Hamcrest → assertThat(古い)

✅ assertThatとassertionsはどちらか統一したほうがいい

・特に 1つのテストクラス内では必ず統一し、プロジェクト全体でも原則として AssertJ の assertThat に統一するのが最も合理的です。

◾️根拠

1.可読性向上(最重要)

// NG(混在)
assertThat(user.getName()).isEqualTo("Alice");
Assertions.assertEquals("Alice", user.getName());

// OK(統一)
assertThat(user.getName()).isEqualTo("Alice");

2.表現力の差(設計的観点)

・assertThat(AssertJ)は Fluent Interface により、ドメインに近い表現が可能

assertThat(users)
    .hasSize(3)
    .extracting(User::getName)
    .containsExactly("Alice", "Bob", "Charlie");

✅ extractingとは

assertThat(users)
    .extracting(User::getName)
    .contains("Alice", "Bob");

・Userオブジェクトの中から
・nameだけを抽出(extracting)して検証

✅ containsonlyとは

◾️結果
・「コレクションの中身が**指定した要素だけで構成されているか(順序は無視)」を検証するアサーションです。主に AssertJ で使われます。

◾️根拠
containsOnlyは以下の条件を同時に満たすときに成功します:
・指定した要素がすべて含まれている
・それ以外の要素が一切含まれていない
・順番は関係ない
・つまり「集合として完全一致(順序なし)」です。

◾️具体例

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

List<String> actual = List.of("A", "B", "C");

// OK(順序違ってもOK)
assertThat(actual).containsOnly("C", "B", "A");

// NG(Dがない)
assertThat(actual).containsOnly("A", "B", "C", "D");

// NG(余計な要素がある場合)
List<String> actual2 = List.of("A", "B", "C", "D");
assertThat(actual2).containsOnly("A", "B", "C");

◾️比較

List<String> actual = List.of("A", "B", "C");

// contains → ゆるい
assertThat(actual).contains("A", "B"); // OK

// containsOnly → 厳密(順序無視)
assertThat(actual).containsOnly("B", "A", "C"); // OK

// containsExactly → 最も厳密(順序も一致)
assertThat(actual).containsExactly("A", "B", "C"); // OK

◾️注意

1.重複は無視される。個数を見ていない(重要)

assertThat(List.of("A", "A", "B")).containsOnly("A", "B"); // OK

2.個数まで検証したいなら

assertThat(actual).containsExactlyInAnyOrder("A", "B", "C");

3.Setと相性が良い
→ 順序を持たないデータ構造と思想が一致

✅入力チェックテストとの異常系と正常系について

◾️結果

・正常系(Happy Path)
→ すべての入力が仕様どおり → 正常に処理されることを確認
・異常系(Validation Error)
→ 入力値がルール違反 → バリデーションエラーが発生することを確認
・境界値・例外系(Edge / Corner Case)
→ ギリギリの値・特殊条件 → 想定どおりの挙動になるか確認

・この3分類を意識しないと、テストは必ず「抜け漏れ」か「重複」で破綻します。

◾️ドメインサンプル

public class User {

    @NotBlank
    @Size(min = 3, max = 10)
    private String name;

    @Min(0)
    @Max(120)
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}

◾️テストクラス(正常系)

import jakarta.validation.*;
import org.junit.jupiter.api.*;
import java.util.Set;

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

class UserValidationTest {

    private static Validator validator;

    @BeforeAll
    static void setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    // -----------------------------
    // 正常系
    // -----------------------------
    @Test
    void 正常系_有効なユーザー() {
        User user = new User("Taro", 30);

        Set<ConstraintViolation<User>> violations = validator.validate(user);

        assertThat(violations).isEmpty();
    }

◾️テストクラス(異常系)

import jakarta.validation.*;
import org.junit.jupiter.api.*;
import java.util.Set;

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

class UserValidationTest {

    private static Validator validator;

    @BeforeAll
    static void setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    // -----------------------------
    // 異常系(バリデーションエラー)
    // -----------------------------
    @Test
    void 異常系_名前が空() {
        User user = new User("", 30);

        Set<ConstraintViolation<User>> violations = validator.validate(user);

        assertThat(violations).isNotEmpty();
        assertThat(violations)
                .extracting("propertyPath.toString")
                .contains("name");
    }

◾️テストクラス(境界値)

import jakarta.validation.*;
import org.junit.jupiter.api.*;
import java.util.Set;

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

class UserValidationTest {

    private static Validator validator;

    @BeforeAll
    static void setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    // -----------------------------
    // 境界値
    // -----------------------------
    @Test
    void 境界値_名前が最小文字数() {
        User user = new User("abc", 30);

        Set<ConstraintViolation<User>> violations = validator.validate(user);

        assertThat(violations).isEmpty();
    }

    @Test
    void 境界値_名前が最大文字数() {
        User user = new User("abcdefghij", 30);

        Set<ConstraintViolation<User>> violations = validator.validate(user);

        assertThat(violations).isEmpty();
    }

    @Test
    void 境界値_年齢が0() {
        User user = new User("Taro", 0);

        Set<ConstraintViolation<User>> violations = validator.validate(user);

        assertThat(violations).isEmpty();
    }

◾️テストクラス(例外)

import jakarta.validation.*;
import org.junit.jupiter.api.*;
import java.util.Set;

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

class UserValidationTest {

    private static Validator validator;

    @BeforeAll
    static void setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    // -----------------------------
    // 例外系(システム例外)
    // -----------------------------
    @Test
    void 例外系_nullオブジェクト() {
        Assertions.assertThrows(
                IllegalArgumentException.class,
                () -> validator.validate(null)
        );
    }
}

✍️ 次に学習したい事のメモ・感想

今回はControllerクラス,入力チェックのテスト実装のインプットだったのでアウトプットしてアプリに組み込む。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?