unit-test-boundary-conditions について
unit-test-boundary-conditionsは、JUnit 5を使うJava開発者向けのテストスキルです。数値、文字列、コレクション、浮動小数点、日時、配列インデックスの境界を体系的にテストする方法を扱います。
主な機能
- 01数値の限界値とオーバーフローのテスト
- 02null、空文字、空白文字の確認
- 03空および要素のあるコレクションの確認
- 04許容誤差を用いた浮動小数点の検証
- 05日付、時刻、配列の境界テスト
活用シーン
- 01数値の限界値での動作検証
- 02オフバイワンエラーの発見
- 03算術オーバーフローとアンダーフローの確認
- 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/空/空白のみの入力
- オーバーフロー/アンダーフローの検証、コレクションの境界
- オフバイワンエラー、浮動小数点の精度
手順
- 境界を特定する:数値の限界値(MIN_VALUE、MAX_VALUE、ゼロ)、文字列の状態(null、空、空白のみ)、コレクションのサイズ(0、1、多数)を列挙します
- パラメーター化テストを適用する:複数の境界値には
@ValueSourceまたは@CsvSourceと@ParameterizedTestを使用します - 境界の両側をテストする:各境界の直前、境界上、直後の値を網羅します
- 各境界カテゴリを追加した後にテストを実行し、問題を早期に発見します
- 浮動小数点の精度を検証する:AssertJ の
isCloseTo(expected, within(tolerance))を使用します - コレクションの状態をテストする:空(0)、単一(1)、多数(>1)要素の各シナリオを明示的にテストします
- オーバーフロー/アンダーフローに対処する:
Math.addExact()とMath.subtractExact()を使用して算術オーバーフローを検出します - 日付/時刻の境界をテストする:うるう年、月の境界、タイムゾーンの切り替わりを検証します
- 失敗に基づいて反復する:境界テストが失敗した場合は、エラーを分析して未テストの境界を見つけ、新たに判明したエッジ条件のテストケースを追加します
例
必要な依存関係: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、範囲外を必ずテストします
参考資料
コメント
Markdown 対応