home / skills / giuseppe-trisciuoglio / developer-kit / unit-test-boundary-conditions
/plugins/developer-kit-java/skills/unit-test-boundary-conditions
This skill helps you apply comprehensive unit-test boundary patterns, covering min/max values, nulls, empties, precision, and edge cases across Java/Python
npx playbooks add skill giuseppe-trisciuoglio/developer-kit --skill unit-test-boundary-conditionsReview the files below or copy the command above to add this skill to your agents.
---
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.
category: testing
tags: [junit-5, boundary-testing, edge-cases, parameterized-test]
version: 1.0.1
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.
## 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
```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
### 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);
}
}
```
## 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);
}
}
```
## 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
```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);
}
}
```
## 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);
}
}
```
## 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();
}
}
```
## 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
- [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)
- [Boundary Value Analysis](https://en.wikipedia.org/wiki/Boundary-value_analysis)
This skill provides patterns and ready-made approaches for unit testing boundary conditions and edge cases using pure JUnit 5. It focuses on numeric limits, null/empty strings, collection sizes, floating-point precision, date/time edges, array indexing, and basic concurrent scenarios. Use it to make tests explicit about limits and to prevent silent failures like overflow or NaN surprises.
The skill inspects common boundary categories (min/max values, zero, null, empty, whitespace, tiny/huge floats, first/last indices, out-of-bounds, leap dates, timezone transitions, and basic concurrency cases). It recommends parameterized tests, tolerance-based float assertions, and explicit checks just below, at, and just above boundaries. Examples show JUnit 5 annotations, AssertJ assertions, and techniques such as Math.addExact() to detect overflow and CopyOnWrite collections to exercise concurrency behavior.
Should I test every possible boundary value?
Prioritize domain-critical boundaries: test just below, at, and just above each limit rather than every possible value.
How should I compare floating point results?
Never use exact equality; assert with a tolerance (for example AssertJ's isCloseTo within a small delta) appropriate to the domain.