テスト設計をJavaで保守しやすくする実務ポイント

Javaのテスト設計で単体テスト、境界値、モック、結合テストを整理する図

Javaのテスト設計は仕様と変更点から考える

Javaのテスト設計で迷う場面は、実務ではかなり多いです。JUnitでテストを書けることと、保守しやすいテストを設計できることは別です。何を単体テストで確認するか。モックをどこまで使うか。境界値や異常系をどう選ぶか。こうした判断が、後から効いてきます。

結論から言うと、テストは実装行数をなぞるものではありません。そのため、仕様、境界、失敗時の振る舞い、変更されやすい判断を確認するために書きます。

この記事では、Java/Spring Bootの実務で使いやすい考え方を整理します。また、単体テスト、境界値、モック、結合テスト、レビューで見られやすいポイントを扱います。

単体テストはメソッドではなく振る舞いを見る

まず、単体テストは「publicメソッドごとに1つ書く」という作業ではありません。大切なのは、そのクラスやメソッドが外から見てどのように振る舞うかです。

例えば、料金計算のテストなら、内部でどのprivateメソッドを呼ぶかより、入力条件に対して正しい金額、割引、例外が返るかを確認します。実装の都合に寄せすぎると、リファクタリングのたびに壊れるテストになります。

JUnitの基本的な書き方は、公式のJUnit User Guideで確認できます。ただし、実務で重要なのはアノテーションの知識だけではありません。つまり、どの観点をテストケースとして残すかです。

class PriceCalculatorTest {

  @Test
  void calculatesDiscountedPriceForGoldMember() {
    PriceCalculator calculator = new PriceCalculator();

    Money actual = calculator.calculate(
        new OrderAmount(10_000),
        MemberRank.GOLD
    );

    assertThat(actual).isEqualTo(new Money(9_000));
  }
}

このテストでは、メソッド内部の分岐ではなく「ゴールド会員なら10%割引される」という業務の振る舞いを見ています。そのため、内部実装を変えても仕様が同じならテストは残せます。

Javaのテスト設計では境界値を先に洗い出す

次に、境界値を先に洗い出します。なぜなら、テストケースを思いつきで増やすと、数は多いのに重要な条件が抜けることがあるためです。

例えば、購入金額が10,000円以上なら送料無料という仕様なら、見るべきなのは9,999円、10,000円、10,001円です。正常系を1つだけ確認しても、境界の実装ミスは見つかりません。

観点確認する例意図
境界値9,999円、10,000円、10,001円条件分岐の境目を確認する
代表値通常会員、ゴールド会員主要な業務パターンを確認する
異常系負数、null、不正な状態失敗時の振る舞いを確認する
組み合わせ会員ランクとキャンペーン条件の衝突を確認する

ただし、すべての組み合わせを網羅しようとするとテストは膨らみます。業務上の影響が大きい条件、過去にバグが出た条件、今後変更されやすい条件を優先します。

テストケースは正常系だけで終わらせない

正常系のテストだけでは、実務では不十分です。例えば、業務アプリでは、入力不備、権限不足、在庫不足、外部API失敗など、失敗時の扱いが重要になります。

特に、例外を投げるのか、エラーコードを返すのか、空の結果を返すのかは、呼び出し元の実装に影響します。ここをテストで固定しておくと、変更時のレビューが楽になります。

@Test
void throwsExceptionWhenStockIsInsufficient() {
  OrderService service = new OrderService(stockRepository);

  assertThatThrownBy(() -> service.placeOrder(request))
      .isInstanceOf(OutOfStockException.class)
      .hasMessageContaining("stock");
}

AssertJのようなアサーションライブラリを使うと、期待値を読みやすく表現できます。詳しい使い方はAssertJのドキュメントで確認できます。

なお、異常系を増やす目的は、単にカバレッジを上げることではありません。障害時や不正入力時に、システムがどのように振る舞うべきかを明確にすることです。

Javaのテスト設計でモックを使いすぎない

モックは便利ですが、使いすぎるとテストが実装詳細に張り付きます。そのため、Repository、外部API、時刻、乱数のように、テスト内で制御したい依存を置き換える用途に絞ると扱いやすいです。

Mockitoを使う場合は、公式サイトのMockito documentationも参考になります。ただし、verifyを増やしすぎると、何を保証したいテストなのかが読みにくくなります。

依存の種類モック候補か考え方
外部APIクライアント候補になる通信結果を固定したい
Repository単体テストでは候補DBを使わず業務ロジックを確認したい
値オブジェクト基本は不要本物を使った方が読みやすい
同じクラス内のprivateメソッド不要外から見える振る舞いで確認する

例えば、ServiceのテストでRepositoryをモックするのは自然です。一方で、ドメインロジックを持つ値オブジェクトまでモックすると、テストが本来の仕様から離れてしまいます。

結合テストとの役割を分ける

単体テストで確認することと、結合テストで確認することは分けます。例えば、すべてを単体テストで確認しようとすると、モックが増えます。逆に、すべてを結合テストに寄せると、実行が遅くなり、失敗原因も追いにくくなります。

実務では、業務ロジックの分岐は単体テストで薄く速く確認します。一方で、DBマッピング、トランザクション、HTTPレスポンス、Springの設定は結合テストで確認する方が向いています。

テスト種別向いている確認注意点
単体テスト計算、分岐、例外、境界値実装詳細に寄せすぎない
RepositoryテストSQL、JPAマッピング、制約テストデータの管理が必要
APIテストHTTPステータス、レスポンス形式ケースを増やしすぎない
E2Eテスト主要な業務フロー遅く壊れやすいため数を絞る

つまり、テストの粒度は目的から決めます。速く何度も回したい確認は単体テストへ寄せ、フレームワークやDBを含めないと意味がない確認は結合テストへ寄せます。

テスト名は仕様が読める形にする

テスト名は、将来の読者に向けたドキュメントでもあります。そのため、実装者本人だけがわかる名前にすると、失敗したときに何が壊れたのか判断しにくくなります。

例えば、testCalculate よりも、calculatesDiscountedPriceForGoldMember の方が意図を読みやすいです。日本語のチームなら、表示名に日本語を使う選択肢もあります。

@Test
@DisplayName("ゴールド会員の場合は10%割引される")
void goldMemberDiscount() {
  // ...
}

ただし、名前を長くすればよいわけではありません。条件、操作、期待結果が伝わる範囲で十分です。失敗時のログに出たとき、何の仕様が壊れたかがわかる名前を目指します。

Javaのテスト設計でレビュー指摘されやすい例

レビューでは、テストがあるかどうかだけでなく、変更に耐えられるかを見ます。特に、次のようなテストは、短期的には動いても保守で困りやすいです。

指摘されやすい例起きる問題見直すポイント
正常系しかない失敗時の仕様がわからない異常系と境界値を追加する
privateメソッドを直接テストしたがる実装変更に弱いpublicな振る舞いで確認する
verifyばかりで結果を見ていない呼び出し順に依存する最終的な状態や戻り値を見る
1つのテストで多くを確認する失敗原因が読みにくい観点ごとに分ける
テストデータが複雑すぎる何を確認しているか埋もれる必要な値だけ明示する

特に、カバレッジの数字だけを目的にすると、意味の薄いテストが増えます。カバレッジは補助指標として使い、レビューでは「このテストでどの仕様を守っているか」を説明できる状態にします。

実務で使うJavaのテスト設計のチェックリスト

  • テスト対象の仕様を一文で説明できるか?
  • 正常系だけでなく、境界値と異常系を確認しているか?
  • モックは外部依存や制御したい依存に絞っているか?
  • 実装詳細ではなく、外から見える振る舞いを確認しているか?
  • 単体テストと結合テストの役割が分かれているか?
  • テスト名から条件と期待結果が読めるか?
  • 失敗したときに原因を追いやすい粒度になっているか?

まずは、このチェックリストで既存のテストを見直します。そのうえで、変更が多い業務ロジックから優先してテストを整えると、効果を実感しやすくなります。

Javaのテスト設計でよくある質問

カバレッジは何パーセントを目指すべきですか?

一律の正解はありません。重要なのは、業務上壊れると困る仕様、分岐、境界値がテストされていることです。ただし、数字を目標にする場合でも、カバレッジだけで品質を判断しないようにします。

privateメソッドはテストしなくてよいですか?

基本的には、privateメソッドを直接テストするより、publicな振る舞いを通して確認します。どうしても直接確認したいほど複雑なら、責務を別クラスへ切り出すサインかもしれません。

モックを使うとテストが壊れやすいのはなぜですか?

呼び出し回数や呼び出し順を細かく検証しすぎると、実装を少し変えただけでテストが落ちます。モックは外部依存を制御するために使い、業務上の期待結果は戻り値、状態、例外で確認する方が安定します。

単体テストと結合テストはどちらを増やすべきですか?

まずは単体テストで業務ロジックの分岐を速く確認できる状態を作ります。そのうえで、DB、HTTP、Spring設定など、単体では意味が薄い部分を結合テストで補います。

まとめ:Javaのテスト設計は変更に強い仕様書を作ること

Javaのテスト設計では、JUnitやMockitoの使い方だけを覚えても不十分です。まず、何を守るためのテストなのかを決めます。仕様、境界値、異常系、外部依存、結合テストとの分担を整理することが重要です。

まずは、変更されやすい業務ロジックから見直します。次に、境界値と失敗時の振る舞いをテストで固定します。さらに、モックを使いすぎず、単体テストと結合テストの役割を分けます。この流れを守ると、レビューしやすく保守しやすいテストに近づきます。

Java/Spring Bootの実務では、テスト、例外処理、API設計、トランザクション設計がつながります。bluenaの採用情報では、Java経験を活かせる案件相談も行っています。今の経験を次の環境でどう伸ばすか、一度整理してみてください。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア