git:20260316.91dc3c4 to git:20260324.a02e282

52 added, 249 removed. Audit A to A.

---
name: unit-test-boundary-conditions
- description: Edge case and boundary testing patterns for unit tests. Testing minimum/maximum values, null cases, empty collections, and numeric precision. Pure JUnit 5 unit tests. Use when ensuring code handles limits and special cases correctly.
+ description: Provides edge case, corner case, boundary condition, and limit testing patterns for Java unit tests. Validates minimum/maximum values, null cases, empty collections, numeric overflow/underflow, floating-point precision, and off-by-one scenarios using JUnit 5 and AssertJ. Use when writing .java test files to ensure code handles limits, corner cases, and special inputs correctly.
allowed-tools: Read, Write, Bash, Glob, Grep
---
# 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.
+ Systematic patterns for testing boundary conditions, corner cases, and limit values in Java using JUnit 5. Covers numeric boundaries, string edge cases, collection states, floating-point precision, date/time limits, and off-by-one scenarios.
## 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
+ - Numeric min/max limits, null/empty/whitespace inputs
+ - Overflow/underflow validation, collection boundaries
+ - Off-by-one errors, floating-point precision
## 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
+ 1. **Identify boundaries**: List numeric limits (MIN_VALUE, MAX_VALUE, zero), string states (null, empty, whitespace), collection sizes (0, 1, many)
+ 2. **Apply parameterized tests**: Use `@ParameterizedTest` with `@ValueSource` or `@CsvSource` for multiple boundary values
+ 3. **Test both sides of boundaries**: Cover values just below, at, and just above each boundary
+ 4. **Run tests after adding each boundary category** to catch issues early
+ 5. **Verify floating-point precision**: Use `isCloseTo(expected, within(tolerance))` with AssertJ
+ 6. **Test collection states**: Explicitly test empty (0), single (1), and many (>1) element scenarios
+ 7. **Handle overflow/underflow**: Use `Math.addExact()` and `Math.subtractExact()` to detect arithmetic overflow
+ 8. **Test date/time edges**: Verify leap years, month boundaries, timezone transitions
+ 9. **Iterate based on failures**: When a boundary test fails, analyze the error to discover additional untested boundaries; add test cases for the newly discovered edge conditions
## Examples
- ## Setup: Boundary Testing
-
- ### 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>
- ```
-
- ### Gradle
- ```kotlin
- dependencies {
- testImplementation("org.junit.jupiter:junit-jupiter")
- testImplementation("org.junit.jupiter:junit-jupiter-params")
- testImplementation("org.assertj:assertj-core")
- }
- ```
-
- ## Numeric Boundary Testing
+ Requires: `junit-jupiter`, `junit-jupiter-params`, `assertj-core`.
- ### Integer Limits
+ ## Integer Boundary Testing
```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
-
+ void shouldDetectIntegerOverflow() {
assertThatThrownBy(() -> Math.addExact(Integer.MAX_VALUE, 1))
.isInstanceOf(ArithmeticException.class);
}
@Test
- void shouldHandleIntegerUnderflow() {
+ void shouldDetectIntegerUnderflow() {
assertThatThrownBy(() -> Math.subtractExact(Integer.MIN_VALUE, 1))
.isInstanceOf(ArithmeticException.class);
}
@Test
- void shouldHandleZero() {
+ void shouldHandleZeroEdge() {
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) {
+ 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() {
- String result = StringUtils.capitalize("a");
- assertThat(result).isEqualTo("A");
-
- String result2 = StringUtils.trim("x");
- assertThat(result2).isEqualTo("x");
+ 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();
}
-
- @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);
- }
}
```
- ## Floating Point Boundary Testing
-
- ### Precision and Special Values
+ ## Floating-Point Boundary Testing
```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();
+ assertThat(Double.NaN).isNotEqualTo(Double.NaN);
}
@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();
+ assertThat(1.0 / 0.0).isEqualTo(Double.POSITIVE_INFINITY);
+ assertThat(-1.0 / 0.0).isEqualTo(Double.NEGATIVE_INFINITY);
+ assertThat(0.0 / 0.0).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() {
+ void shouldRejectInvalidDateInNonLeapYear() {
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);
+ assertThatThrownBy(() -> array[-1])
+ .isInstanceOf(ArrayIndexOutOfBoundsException.class);
}
@Test
void shouldThrowOnOutOfBoundsIndex() {
int[] array = {1, 2, 3};
-
- assertThatThrownBy(() -> {
- int value = array[10];
- }).isInstanceOf(ArrayIndexOutOfBoundsException.class);
+ assertThatThrownBy(() -> 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();
- }
- }
- ```
-
- ## 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();
+ assertThatThrownBy(() -> empty[0])
+ .isInstanceOf(ArrayIndexOutOfBoundsException.class);
}
}
```
## Best Practices
- - **Test explicitly at boundaries** - don't rely on random testing
+ - **Test at boundaries explicitly**: 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
+ - **Verify error messages** for invalid boundary inputs
+ - **Document why** specific boundaries matter for your domain
+ - **Test overflow/underflow** for all 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.
+ - **Integer overflow**: Use `Math.addExact()` to detect silent overflow
+ - **Floating-point precision**: Never use exact equality; always use tolerance-based assertions
+ - **NaN behavior**: `NaN != NaN`; use `Float.isNaN()` or `Double.isNaN()`
+ - **Collection size limits**: Be mindful of memory with large test collections
+ - **String encoding**: Test with Unicode characters for internationalization
+ - **Date/time boundaries**: Account for timezone transitions and daylight saving
+ - **Array indexing**: Always test index 0, length-1, and out-of-bounds
## References
- [Integer.MIN_VALUE/MAX_VALUE](https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html)
- [Double.MIN_VALUE/MAX_VALUE](https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html)
- - [AssertJ Floating Point Assertions](https://assertj.github.io/assertj-core-features-highlight.html#assertions-on-numbers)
+ - [AssertJ Floating Point](https://assertj.github.io/assertj-core-features-highlight.html#assertions-on-numbers)
- [Boundary Value Analysis](https://en.wikipedia.org/wiki/Boundary-value_analysis)
+ - [references/concurrent-testing.md](references/concurrent-testing.md) - Thread safety patterns
+ - [references/parameterized-patterns.md](references/parameterized-patterns.md) - Off-by-one and parameterized examples