unit-test-boundary-conditions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing Boundary Conditions and Edge Cases

单元测试的边界条件与边缘案例

Test boundary conditions, edge cases, and limit values systematically. Verify code behavior at limits, with null/empty inputs, and overflow scenarios.
系统地测试边界条件、边缘案例和极限值。验证代码在极限值、空值/空输入以及溢出场景下的行为。

When to Use This Skill

何时使用该测试技巧

Use this skill when:
  • Testing minimum and maximum values
  • Testing null and empty inputs
  • Testing whitespace-only strings
  • Testing overflow/underflow scenarios
  • Testing collections with zero/one/many items
  • Verifying behavior at API boundaries
  • Want comprehensive edge case coverage
在以下场景使用该技巧:
  • 测试最小值和最大值
  • 测试空值和空输入
  • 测试仅含空白字符的字符串
  • 测试溢出/下溢场景
  • 测试包含0个、1个或多个元素的集合
  • 验证API边界处的行为
  • 需要全面的边缘案例覆盖

Setup: Boundary Testing

环境搭建:边界测试

Maven

Maven

xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-params</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>
xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-params</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>

Gradle

Gradle

kotlin
dependencies {
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.junit.jupiter:junit-jupiter-params")
  testImplementation("org.assertj:assertj-core")
}
kotlin
dependencies {
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.junit.jupiter:junit-jupiter-params")
  testImplementation("org.assertj:assertj-core")
}

Numeric Boundary Testing

数值边界测试

Integer Limits

整数极限值

java
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 shouldHandleIntegerOverflow() {
    int maxInt = Integer.MAX_VALUE;
    int result = Math.addExact(maxInt, 1); // Will throw ArithmeticException
    
    assertThatThrownBy(() -> Math.addExact(Integer.MAX_VALUE, 1))
      .isInstanceOf(ArithmeticException.class);
  }

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

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

    assertThatThrownBy(() -> MathUtils.divide(5, 0))
      .isInstanceOf(ArithmeticException.class);
  }
}
java
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 shouldHandleIntegerOverflow() {
    int maxInt = Integer.MAX_VALUE;
    int result = Math.addExact(maxInt, 1); // Will throw ArithmeticException
    
    assertThatThrownBy(() -> Math.addExact(Integer.MAX_VALUE, 1))
      .isInstanceOf(ArithmeticException.class);
  }

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

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

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

String Boundary Testing

字符串边界测试

Null, Empty, and Whitespace

空值、空字符串与空白字符

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

class StringBoundaryTest {

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

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

  @Test
  void shouldHandleSingleCharacter() {
    String result = StringUtils.capitalize("a");
    assertThat(result).isEqualTo("A");

    String result2 = StringUtils.trim("x");
    assertThat(result2).isEqualTo("x");
  }

  @Test
  void shouldHandleVeryLongString() {
    String longString = "x".repeat(1000000);
    
    assertThat(longString.length()).isEqualTo(1000000);
    assertThat(StringUtils.isNotBlank(longString)).isTrue();
  }

  @Test
  void shouldHandleSpecialCharacters() {
    String special = "!@#$%^&*()_+-={}[]|\\:;<>?,./";
    
    assertThat(StringUtils.length(special)).isEqualTo(31);
  }
}
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class StringBoundaryTest {

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

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

  @Test
  void shouldHandleSingleCharacter() {
    String result = StringUtils.capitalize("a");
    assertThat(result).isEqualTo("A");

    String result2 = StringUtils.trim("x");
    assertThat(result2).isEqualTo("x");
  }

  @Test
  void shouldHandleVeryLongString() {
    String longString = "x".repeat(1000000);
    
    assertThat(longString.length()).isEqualTo(1000000);
    assertThat(StringUtils.isNotBlank(longString)).isTrue();
  }

  @Test
  void shouldHandleSpecialCharacters() {
    String special = "!@#$%^&*()_+-={}[]|\\:;<>?,./";
    
    assertThat(StringUtils.length(special)).isEqualTo(31);
  }
}

Collection Boundary Testing

集合边界测试

Empty, Single, and Large Collections

空集合、单元素集合与大集合

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

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);
  }

  @Test
  void shouldHandleDuplicatesInCollection() {
    List<Integer> duplicates = List.of(1, 1, 2, 2, 3, 3);
    
    assertThat(duplicates).hasSize(6);
    Set<Integer> unique = new HashSet<>(duplicates);
    assertThat(unique).hasSize(3);
  }
}
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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);
  }

  @Test
  void shouldHandleDuplicatesInCollection() {
    List<Integer> duplicates = List.of(1, 1, 2, 2, 3, 3);
    
    assertThat(duplicates).hasSize(6);
    Set<Integer> unique = new HashSet<>(duplicates);
    assertThat(unique).hasSize(3);
  }
}

Floating Point Boundary Testing

浮点数边界测试

Precision and Special Values

精度与特殊值

java
class FloatingPointBoundaryTest {

  @Test
  void shouldHandleFloatingPointPrecision() {
    double result = 0.1 + 0.2;
    
    // Floating point comparison needs tolerance
    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); // NaN != NaN
  }

  @Test
  void shouldHandleVerySmallAndLargeNumbers() {
    double tiny = Double.MIN_VALUE;
    double huge = Double.MAX_VALUE;

    assertThat(tiny).isGreaterThan(0);
    assertThat(huge).isPositive();
  }

  @Test
  void shouldHandleZeroInDivision() {
    double result = 1.0 / 0.0;
    
    assertThat(result).isEqualTo(Double.POSITIVE_INFINITY);

    double result2 = -1.0 / 0.0;
    assertThat(result2).isEqualTo(Double.NEGATIVE_INFINITY);

    double result3 = 0.0 / 0.0;
    assertThat(result3).isNaN();
  }
}
java
class FloatingPointBoundaryTest {

  @Test
  void shouldHandleFloatingPointPrecision() {
    double result = 0.1 + 0.2;
    
    // Floating point comparison needs tolerance
    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); // NaN != NaN
  }

  @Test
  void shouldHandleVerySmallAndLargeNumbers() {
    double tiny = Double.MIN_VALUE;
    double huge = Double.MAX_VALUE;

    assertThat(tiny).isGreaterThan(0);
    assertThat(huge).isPositive();
  }

  @Test
  void shouldHandleZeroInDivision() {
    double result = 1.0 / 0.0;
    
    assertThat(result).isEqualTo(Double.POSITIVE_INFINITY);

    double result2 = -1.0 / 0.0;
    assertThat(result2).isEqualTo(Double.NEGATIVE_INFINITY);

    double result3 = 0.0 / 0.0;
    assertThat(result3).isNaN();
  }
}

Date/Time Boundary Testing

日期/时间边界测试

Min/Max Dates and Edge Cases

最小/最大日期与边缘场景

java
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();
    assertThat(LocalDate.of(2024, 2, 29)).isEqualTo(leapYearEnd);
  }

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

  @Test
  void shouldHandleYearBoundaries() {
    LocalDate newYear = LocalDate.of(2024, 1, 1);
    LocalDate lastDay = LocalDate.of(2024, 12, 31);

    assertThat(newYear).isBefore(lastDay);
  }

  @Test
  void shouldHandleMidnightBoundary() {
    LocalTime midnight = LocalTime.MIDNIGHT;
    LocalTime almostMidnight = LocalTime.of(23, 59, 59);

    assertThat(almostMidnight).isBefore(midnight);
  }
}
java
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();
    assertThat(LocalDate.of(2024, 2, 29)).isEqualTo(leapYearEnd);
  }

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

  @Test
  void shouldHandleYearBoundaries() {
    LocalDate newYear = LocalDate.of(2024, 1, 1);
    LocalDate lastDay = LocalDate.of(2024, 12, 31);

    assertThat(newYear).isBefore(lastDay);
  }

  @Test
  void shouldHandleMidnightBoundary() {
    LocalTime midnight = LocalTime.MIDNIGHT;
    LocalTime almostMidnight = LocalTime.of(23, 59, 59);

    assertThat(almostMidnight).isBefore(midnight);
  }
}

Array Index Boundary Testing

数组索引边界测试

First, Last, and Out of Bounds

第一个、最后一个与越界索引

java
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(() -> {
      int value = array[-1];
    }).isInstanceOf(ArrayIndexOutOfBoundsException.class);
  }

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

  @Test
  void shouldHandleEmptyArray() {
    int[] empty = {};
    
    assertThat(empty.length).isZero();
    assertThatThrownBy(() -> {
      int value = empty[0];
    }).isInstanceOf(ArrayIndexOutOfBoundsException.class);
  }
}
java
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(() -> {
      int value = array[-1];
    }).isInstanceOf(ArrayIndexOutOfBoundsException.class);
  }

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

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

Concurrent and Thread Boundary Testing

并发与线程边界测试

Null and Race Conditions

空值与竞态条件

java
import java.util.concurrent.*;

class ConcurrentBoundaryTest {

  @Test
  void shouldHandleNullInConcurrentMap() {
    ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
    
    map.put("key", "value");
    assertThat(map.get("nonexistent")).isNull();
  }

  @Test
  void shouldHandleConcurrentModification() {
    List<Integer> list = new CopyOnWriteArrayList<>(List.of(1, 2, 3, 4, 5));
    
    // Should not throw ConcurrentModificationException
    for (int num : list) {
      if (num == 3) {
        list.add(6);
      }
    }

    assertThat(list).hasSize(6);
  }

  @Test
  void shouldHandleEmptyBlockingQueue() throws InterruptedException {
    BlockingQueue<String> queue = new LinkedBlockingQueue<>();
    
    assertThat(queue.poll()).isNull();
  }
}
java
import java.util.concurrent.*;

class ConcurrentBoundaryTest {

  @Test
  void shouldHandleNullInConcurrentMap() {
    ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
    
    map.put("key", "value");
    assertThat(map.get("nonexistent")).isNull();
  }

  @Test
  void shouldHandleConcurrentModification() {
    List<Integer> list = new CopyOnWriteArrayList<>(List.of(1, 2, 3, 4, 5));
    
    // Should not throw ConcurrentModificationException
    for (int num : list) {
      if (num == 3) {
        list.add(6);
      }
    }

    assertThat(list).hasSize(6);
  }

  @Test
  void shouldHandleEmptyBlockingQueue() throws InterruptedException {
    BlockingQueue<String> queue = new LinkedBlockingQueue<>();
    
    assertThat(queue.poll()).isNull();
  }
}

Parameterized Boundary Testing

参数化边界测试

Multiple Boundary Cases

多边界案例

java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class ParameterizedBoundaryTest {

  @ParameterizedTest
  @CsvSource({
    "null,            false", // null
    "'',              false", // empty
    "'   ',           false", // whitespace
    "a,               true",  // single char
    "abc,             true"   // normal
  })
  void shouldValidateStringBoundaries(String input, boolean expected) {
    boolean result = StringValidator.isValid(input);
    assertThat(result).isEqualTo(expected);
  }

  @ParameterizedTest
  @ValueSource(ints = {Integer.MIN_VALUE, 0, 1, -1, Integer.MAX_VALUE})
  void shouldHandleNumericBoundaries(int value) {
    assertThat(value).isNotNull();
  }
}
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class ParameterizedBoundaryTest {

  @ParameterizedTest
  @CsvSource({
    "null,            false", // null
    "'',              false", // empty
    "'   ',           false", // whitespace
    "a,               true",  // single char
    "abc,             true"   // normal
  })
  void shouldValidateStringBoundaries(String input, boolean expected) {
    boolean result = StringValidator.isValid(input);
    assertThat(result).isEqualTo(expected);
  }

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

Best Practices

最佳实践

  • Test explicitly at boundaries - don't rely on random testing
  • Test null and empty separately from valid inputs
  • Use parameterized tests for multiple boundary cases
  • Test both sides of boundaries (just below, at, just above)
  • Verify error messages are helpful for invalid boundaries
  • Document why specific boundaries matter
  • Test overflow/underflow for numeric operations
  • 明确测试边界 - 不要依赖随机测试
  • 将空值和空输入与有效输入分开测试
  • 对多个边界案例使用参数化测试
  • 测试边界的两侧(略低于、等于、略高于边界值)
  • 验证错误信息对无效边界场景有帮助
  • 记录特定边界的重要性原因
  • 测试数值运算的溢出/下溢

Common Pitfalls

常见误区

  • Testing only "happy path" without boundary cases
  • Forgetting null/empty cases
  • Not testing floating point precision
  • Not testing collection boundaries (empty, single, many)
  • Not testing string boundaries (null, empty, whitespace)
  • 仅测试“愉快路径”而忽略边界案例
  • 忘记测试空值/空场景
  • 未测试浮点数精度
  • 未测试集合边界(空、单元素、多元素)
  • 未测试字符串边界(空值、空字符串、空白字符)

Troubleshooting

故障排查

Floating point comparison fails: Use
isCloseTo(expected, within(tolerance))
.
Collection boundaries unclear: List cases explicitly: empty (0), single (1), many (>1).
Date boundary confusing: Use
LocalDate.MIN
,
LocalDate.MAX
for clear boundaries.
浮点数比较失败:使用
isCloseTo(expected, within(tolerance))
方法。
集合边界不清晰:明确列出场景:空集合(0个元素)、单元素集合(1个元素)、多元素集合(>1个元素)。
日期边界混淆:使用
LocalDate.MIN
LocalDate.MAX
来明确边界。

References

参考资料