mcpField
U
unit-test-boundary-conditions

unit-test-boundary-conditions

github.com/giuseppe-trisciuoglioテスト・品質3654.1k 回インストール7 週間前に更新
リポジトリ

unit-test-boundary-conditions について

unit-test-boundary-conditionsは、JUnit 5を使うJava開発者向けのテストスキルです。数値、文字列、コレクション、浮動小数点、日時、配列インデックスの境界を体系的にテストする方法を扱います。

主な機能

  1. 01数値の限界値とオーバーフローのテスト
  2. 02null、空文字、空白文字の確認
  3. 03空および要素のあるコレクションの確認
  4. 04許容誤差を用いた浮動小数点の検証
  5. 05日付、時刻、配列の境界テスト

活用シーン

  1. 01数値の限界値での動作検証
  2. 02オフバイワンエラーの発見
  3. 03算術オーバーフローとアンダーフローの確認
  4. 04うるう年とタイムゾーン移行のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-boundary-conditions

インストール後、Claude Code で/unit-test-boundary-conditionsを入力して読み込まれたか確認します。

同梱ファイルreferences/concurrent-testing.mdreferences/parameterized-patterns.md

詳細

境界条件とエッジケースの単体テスト

概要

JUnit 5 を使用して、Java の境界条件、コーナーケース、限界値を体系的にテストするパターンです。数値の境界、文字列のエッジケース、コレクションの状態、浮動小数点の精度、日付/時刻の限界、オフバイワンのシナリオを扱います。

使用する場面

  • 数値の最小値/最大値、null/空/空白のみの入力
  • オーバーフロー/アンダーフローの検証、コレクションの境界
  • オフバイワンエラー、浮動小数点の精度

手順

  1. 境界を特定する:数値の限界値(MIN_VALUE、MAX_VALUE、ゼロ)、文字列の状態(null、空、空白のみ)、コレクションのサイズ(0、1、多数)を列挙します
  2. パラメーター化テストを適用する:複数の境界値には @ValueSource または @CsvSource と @ParameterizedTest を使用します
  3. 境界の両側をテストする:各境界の直前、境界上、直後の値を網羅します
  4. 各境界カテゴリを追加した後にテストを実行し、問題を早期に発見します
  5. 浮動小数点の精度を検証する:AssertJ の isCloseTo(expected, within(tolerance)) を使用します
  6. コレクションの状態をテストする:空(0)、単一(1)、多数(>1)要素の各シナリオを明示的にテストします
  7. オーバーフロー/アンダーフローに対処する:Math.addExact() と Math.subtractExact() を使用して算術オーバーフローを検出します
  8. 日付/時刻の境界をテストする:うるう年、月の境界、タイムゾーンの切り替わりを検証します
  9. 失敗に基づいて反復する:境界テストが失敗した場合は、エラーを分析して未テストの境界を見つけ、新たに判明したエッジ条件のテストケースを追加します

例

必要な依存関係:junit-jupiter、junit-jupiter-params、assertj-core。

整数の境界テスト

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.*;

class IntegerBoundaryTest {

  @ParameterizedTest
  @ValueSource(ints = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, 0, Integer.MAX_VALUE - 1, Integer.MAX_VALUE})
  void shouldHandleIntegerBoundaries(int value) {
    assertThat(value).isNotNull();
  }

  @Test
  void shouldDetectIntegerOverflow() {
    assertThatThrownBy(() -> Math.addExact(Integer.MAX_VALUE, 1))
      .isInstanceOf(ArithmeticException.class);
  }

  @Test
  void shouldDetectIntegerUnderflow() {
    assertThatThrownBy(() -> Math.subtractExact(Integer.MIN_VALUE, 1))
      .isInstanceOf(ArithmeticException.class);
  }

  @Test
  void shouldHandleZeroEdge() {
    int result = MathUtils.divide(0, 5);
    assertThat(result).isZero();

    assertThatThrownBy(() -> MathUtils.divide(5, 0))
      .isInstanceOf(ArithmeticException.class);
  }
}

文字列の境界テスト

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class StringBoundaryTest {

  @ParameterizedTest
  @ValueSource(strings = {"", " ", "  ", "\t", "\n"})
  void shouldRejectEmptyAndWhitespace(String input) {
    boolean result = StringUtils.isNotBlank(input);
    assertThat(result).isFalse();
  }

  @Test
  void shouldHandleNullString() {
    String result = StringUtils.trim(null);
    assertThat(result).isNull();
  }

  @Test
  void shouldHandleSingleCharacter() {
    assertThat(StringUtils.capitalize("a")).isEqualTo("A");
    assertThat(StringUtils.trim("x")).isEqualTo("x");
  }

  @Test
  void shouldHandleVeryLongString() {
    String longString = "x".repeat(1000000);

    assertThat(longString.length()).isEqualTo(1000000);
    assertThat(StringUtils.isNotBlank(longString)).isTrue();
  }
}

コレクションの境界テスト

class CollectionBoundaryTest {

  @Test
  void shouldHandleEmptyList() {
    List<String> empty = List.of();

    assertThat(empty).isEmpty();
    assertThat(CollectionUtils.first(empty)).isNull();
    assertThat(CollectionUtils.count(empty)).isZero();
  }

  @Test
  void shouldHandleSingleElementList() {
    List<String> single = List.of("only");

    assertThat(single).hasSize(1);
    assertThat(CollectionUtils.first(single)).isEqualTo("only");
    assertThat(CollectionUtils.last(single)).isEqualTo("only");
  }

  @Test
  void shouldHandleLargeList() {
    List<Integer> large = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
      large.add(i);
    }

    assertThat(large).hasSize(100000);
    assertThat(CollectionUtils.first(large)).isZero();
    assertThat(CollectionUtils.last(large)).isEqualTo(99999);
  }

  @Test
  void shouldHandleNullInCollection() {
    List<String> withNull = new ArrayList<>(List.of("a", null, "c"));

    assertThat(withNull).contains(null);
    assertThat(CollectionUtils.filterNonNull(withNull)).hasSize(2);
  }
}

浮動小数点の境界テスト

class FloatingPointBoundaryTest {

  @Test
  void shouldHandleFloatingPointPrecision() {
    double result = 0.1 + 0.2;
    assertThat(result).isCloseTo(0.3, within(0.0001));
  }

  @Test
  void shouldHandleSpecialFloatingPointValues() {
    assertThat(Double.POSITIVE_INFINITY).isGreaterThan(Double.MAX_VALUE);
    assertThat(Double.NEGATIVE_INFINITY).isLessThan(Double.MIN_VALUE);
    assertThat(Double.NaN).isNotEqualTo(Double.NaN);
  }

  @Test
  void shouldHandleZeroInDivision() {
    assertThat(1.0 / 0.0).isEqualTo(Double.POSITIVE_INFINITY);
    assertThat(-1.0 / 0.0).isEqualTo(Double.NEGATIVE_INFINITY);
    assertThat(0.0 / 0.0).isNaN();
  }
}

日付/時刻の境界テスト

class DateTimeBoundaryTest {

  @Test
  void shouldHandleMinAndMaxDates() {
    LocalDate min = LocalDate.MIN;
    LocalDate max = LocalDate.MAX;

    assertThat(min).isBefore(max);
    assertThat(DateUtils.isValid(min)).isTrue();
    assertThat(DateUtils.isValid(max)).isTrue();
  }

  @Test
  void shouldHandleLeapYearBoundary() {
    LocalDate leapYearEnd = LocalDate.of(2024, 2, 29);
    assertThat(leapYearEnd).isNotNull();
  }

  @Test
  void shouldRejectInvalidDateInNonLeapYear() {
    assertThatThrownBy(() -> LocalDate.of(2023, 2, 29))
      .isInstanceOf(DateTimeException.class);
  }
}

配列インデックスの境界テスト

class ArrayBoundaryTest {

  @Test
  void shouldHandleFirstElementAccess() {
    int[] array = {1, 2, 3, 4, 5};
    assertThat(array[0]).isEqualTo(1);
  }

  @Test
  void shouldHandleLastElementAccess() {
    int[] array = {1, 2, 3, 4, 5};
    assertThat(array[array.length - 1]).isEqualTo(5);
  }

  @Test
  void shouldThrowOnNegativeIndex() {
    int[] array = {1, 2, 3};
    assertThatThrownBy(() -> array[-1])
      .isInstanceOf(ArrayIndexOutOfBoundsException.class);
  }

  @Test
  void shouldThrowOnOutOfBoundsIndex() {
    int[] array = {1, 2, 3};
    assertThatThrownBy(() -> array[10])
      .isInstanceOf(ArrayIndexOutOfBoundsException.class);
  }

  @Test
  void shouldHandleEmptyArray() {
    int[] empty = {};
    assertThat(empty.length).isZero();
    assertThatThrownBy(() -> empty[0])
      .isInstanceOf(ArrayIndexOutOfBoundsException.class);
  }
}

ベストプラクティス

  • 境界を明示的にテストする:ランダムテストに頼らないでください
  • 有効な入力とは別に null と空をテストする
  • 複数の境界ケースにはパラメーター化テストを使用する
  • 境界の両側をテストする(直前、境界上、直後)
  • 無効な境界入力に対するエラーメッセージを検証する
  • 特定の境界が重要である理由を記録する
  • すべての数値演算でオーバーフロー/アンダーフローをテストする

制約と警告

  • 整数オーバーフロー:暗黙的なオーバーフローを検出するには Math.addExact() を使用します
  • 浮動小数点の精度:厳密な等値比較は避け、必ず許容誤差に基づくアサーションを使用します
  • NaN の動作:NaN != NaN です。Float.isNaN() または Double.isNaN() を使用します
  • コレクションのサイズ制限:大規模なテストコレクションではメモリに注意してください
  • 文字列エンコーディング:国際化に備えて Unicode 文字を使ったテストを行います
  • 日付/時刻の境界:タイムゾーンの切り替わりと夏時間を考慮します
  • 配列のインデックス:インデックス 0、length-1、範囲外を必ずテストします

参考資料

コメント