unit-test-boundary-conditions

📁 giuseppe-trisciuoglio/developer-kit 📅 10 days ago
98
总安装量
98
周安装量
#2341
全站排名
安装命令
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-boundary-conditions

Agent 安装分布

claude-code 70
replit 64
cursor 64
opencode 58
antigravity 55

Skill 文档

Unit Testing Boundary Conditions and Edge Cases

Overview

This skill provides systematic patterns for testing boundary conditions, edge cases, and limit values using JUnit 5. It covers numeric boundaries (Integer.MIN_VALUE, MAX_VALUE), string edge cases (null, empty, whitespace), collection boundaries, floating-point precision, date/time limits, and concurrent access patterns.

When to Use

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

Instructions

  1. Identify boundaries: List all numeric limits (MIN_VALUE, MAX_VALUE, zero), string states (null, empty, whitespace), and collection sizes (empty, single, many)
  2. Use parameterized tests: Apply @ParameterizedTest with @ValueSource for testing multiple boundary values efficiently
  3. Test both sides of boundaries: Test values just below, at, and just above each boundary
  4. Verify floating point precision: Use isCloseTo(expected, within(tolerance)) for floating point comparisons
  5. Test collection states: Explicitly test empty (0), single (1), and many (>1) element scenarios
  6. Handle overflow scenarios: Use Math.addExact() and Math.subtractExact() to detect overflow/underflow
  7. Test date/time edges: Verify leap years, month boundaries, and timezone transitions
  8. Document boundary rationale: Explain why specific boundaries matter for your domain

Examples

Setup: Boundary Testing

Maven

<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

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

Numeric Boundary Testing

Integer Limits

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

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

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

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

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

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

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

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

Constraints and Warnings

  • Integer overflow: Be aware that integer operations can overflow silently; use Math.addExact() to detect
  • Floating point precision: Never use exact equality for floating point; always use tolerance-based assertions
  • NaN behavior: Remember that NaN != NaN; use Float.isNaN() or Double.isNaN() for detection
  • Collection size limits: Be mindful of memory when testing with very large collections
  • String encoding: Test with various Unicode characters and encodings for internationalization
  • Date/time boundaries: Be aware of timezone transitions and daylight saving time changes
  • Array indexing: Always test index boundaries including 0, length-1, and out-of-bounds scenarios

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.

References