Unit Testing Boundary Conditions and Edge Cases
Overview
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
- Numeric min/max limits, null/empty/whitespace inputs
- Overflow/underflow validation, collection boundaries
- Off-by-one errors, floating-point precision
Instructions
- Identify boundaries: List numeric limits (MIN_VALUE, MAX_VALUE, zero), string states (null, empty, whitespace), collection sizes (0, 1, many)
- Apply parameterized tests: Use
@ParameterizedTest with @ValueSource or @CsvSource for multiple boundary values
- Test both sides of boundaries: Cover values just below, at, and just above each boundary
- Run tests after adding each boundary category to catch issues early
- Verify floating-point precision: Use
isCloseTo(expected, within(tolerance)) with AssertJ
- Test collection states: Explicitly test empty (0), single (1), and many (>1) element scenarios
- Handle overflow/underflow: Use
Math.addExact() and Math.subtractExact() to detect arithmetic overflow
- Test date/time edges: Verify leap years, month boundaries, timezone transitions
- 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
Requires: junit-jupiter, junit-jupiter-params, assertj-core.
Integer Boundary Testing
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);
}
}
String Boundary Testing
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();
}
}
Collection Boundary Testing
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);
}
}
Floating-Point Boundary Testing
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();
}
}
Date/Time Boundary Testing
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);
}
}
Array Index Boundary Testing
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);
}
}
Best Practices
- 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 for invalid boundary inputs
- Document why specific boundaries matter for your domain
- Test overflow/underflow for all numeric operations
Constraints and Warnings
- 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
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: unit-test-boundary-conditions3description: 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.4---56# Unit Testing Boundary Conditions and Edge Cases78## Overview910Systematic 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.1112## When to Use1314- Numeric min/max limits, null/empty/whitespace inputs15- Overflow/underflow validation, collection boundaries16- Off-by-one errors, floating-point precision1718## Instructions19201. **Identify boundaries**: List numeric limits (MIN_VALUE, MAX_VALUE, zero), string states (null, empty, whitespace), collection sizes (0, 1, many)212. **Apply parameterized tests**: Use `@ParameterizedTest` with `@ValueSource` or `@CsvSource` for multiple boundary values223. **Test both sides of boundaries**: Cover values just below, at, and just above each boundary234. **Run tests after adding each boundary category** to catch issues early245. **Verify floating-point precision**: Use `isCloseTo(expected, within(tolerance))` with AssertJ256. **Test collection states**: Explicitly test empty (0), single (1), and many (>1) element scenarios267. **Handle overflow/underflow**: Use `Math.addExact()` and `Math.subtractExact()` to detect arithmetic overflow278. **Test date/time edges**: Verify leap years, month boundaries, timezone transitions289. **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 conditions2930## Examples3132Requires: `junit-jupiter`, `junit-jupiter-params`, `assertj-core`.3334## Integer Boundary Testing3536```java37import org.junit.jupiter.params.ParameterizedTest;38import org.junit.jupiter.params.provider.ValueSource;39import static org.assertj.core.api.Assertions.*;4041class IntegerBoundaryTest {4243 @ParameterizedTest44 @ValueSource(ints = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, 0, Integer.MAX_VALUE - 1, Integer.MAX_VALUE})45 void shouldHandleIntegerBoundaries(int value) {46 assertThat(value).isNotNull();47 }4849 @Test50 void shouldDetectIntegerOverflow() {51 assertThatThrownBy(() -> Math.addExact(Integer.MAX_VALUE, 1))52 .isInstanceOf(ArithmeticException.class);53 }5455 @Test56 void shouldDetectIntegerUnderflow() {57 assertThatThrownBy(() -> Math.subtractExact(Integer.MIN_VALUE, 1))58 .isInstanceOf(ArithmeticException.class);59 }6061 @Test62 void shouldHandleZeroEdge() {63 int result = MathUtils.divide(0, 5);64 assertThat(result).isZero();6566 assertThatThrownBy(() -> MathUtils.divide(5, 0))67 .isInstanceOf(ArithmeticException.class);68 }69}70```7172## String Boundary Testing7374```java75import org.junit.jupiter.params.ParameterizedTest;76import org.junit.jupiter.params.provider.ValueSource;7778class StringBoundaryTest {7980 @ParameterizedTest81 @ValueSource(strings = {"", " ", " ", "\t", "\n"})82 void shouldRejectEmptyAndWhitespace(String input) {83 boolean result = StringUtils.isNotBlank(input);84 assertThat(result).isFalse();85 }8687 @Test88 void shouldHandleNullString() {89 String result = StringUtils.trim(null);90 assertThat(result).isNull();91 }9293 @Test94 void shouldHandleSingleCharacter() {95 assertThat(StringUtils.capitalize("a")).isEqualTo("A");96 assertThat(StringUtils.trim("x")).isEqualTo("x");97 }9899 @Test100 void shouldHandleVeryLongString() {101 String longString = "x".repeat(1000000);102103 assertThat(longString.length()).isEqualTo(1000000);104 assertThat(StringUtils.isNotBlank(longString)).isTrue();105 }106}107```108109## Collection Boundary Testing110111```java112class CollectionBoundaryTest {113114 @Test115 void shouldHandleEmptyList() {116 List<String> empty = List.of();117118 assertThat(empty).isEmpty();119 assertThat(CollectionUtils.first(empty)).isNull();120 assertThat(CollectionUtils.count(empty)).isZero();121 }122123 @Test124 void shouldHandleSingleElementList() {125 List<String> single = List.of("only");126127 assertThat(single).hasSize(1);128 assertThat(CollectionUtils.first(single)).isEqualTo("only");129 assertThat(CollectionUtils.last(single)).isEqualTo("only");130 }131132 @Test133 void shouldHandleLargeList() {134 List<Integer> large = new ArrayList<>();135 for (int i = 0; i < 100000; i++) {136 large.add(i);137 }138139 assertThat(large).hasSize(100000);140 assertThat(CollectionUtils.first(large)).isZero();141 assertThat(CollectionUtils.last(large)).isEqualTo(99999);142 }143144 @Test145 void shouldHandleNullInCollection() {146 List<String> withNull = new ArrayList<>(List.of("a", null, "c"));147148 assertThat(withNull).contains(null);149 assertThat(CollectionUtils.filterNonNull(withNull)).hasSize(2);150 }151}152```153154## Floating-Point Boundary Testing155156```java157class FloatingPointBoundaryTest {158159 @Test160 void shouldHandleFloatingPointPrecision() {161 double result = 0.1 + 0.2;162 assertThat(result).isCloseTo(0.3, within(0.0001));163 }164165 @Test166 void shouldHandleSpecialFloatingPointValues() {167 assertThat(Double.POSITIVE_INFINITY).isGreaterThan(Double.MAX_VALUE);168 assertThat(Double.NEGATIVE_INFINITY).isLessThan(Double.MIN_VALUE);169 assertThat(Double.NaN).isNotEqualTo(Double.NaN);170 }171172 @Test173 void shouldHandleZeroInDivision() {174 assertThat(1.0 / 0.0).isEqualTo(Double.POSITIVE_INFINITY);175 assertThat(-1.0 / 0.0).isEqualTo(Double.NEGATIVE_INFINITY);176 assertThat(0.0 / 0.0).isNaN();177 }178}179```180181## Date/Time Boundary Testing182183```java184class DateTimeBoundaryTest {185186 @Test187 void shouldHandleMinAndMaxDates() {188 LocalDate min = LocalDate.MIN;189 LocalDate max = LocalDate.MAX;190191 assertThat(min).isBefore(max);192 assertThat(DateUtils.isValid(min)).isTrue();193 assertThat(DateUtils.isValid(max)).isTrue();194 }195196 @Test197 void shouldHandleLeapYearBoundary() {198 LocalDate leapYearEnd = LocalDate.of(2024, 2, 29);199 assertThat(leapYearEnd).isNotNull();200 }201202 @Test203 void shouldRejectInvalidDateInNonLeapYear() {204 assertThatThrownBy(() -> LocalDate.of(2023, 2, 29))205 .isInstanceOf(DateTimeException.class);206 }207}208```209210## Array Index Boundary Testing211212```java213class ArrayBoundaryTest {214215 @Test216 void shouldHandleFirstElementAccess() {217 int[] array = {1, 2, 3, 4, 5};218 assertThat(array[0]).isEqualTo(1);219 }220221 @Test222 void shouldHandleLastElementAccess() {223 int[] array = {1, 2, 3, 4, 5};224 assertThat(array[array.length - 1]).isEqualTo(5);225 }226227 @Test228 void shouldThrowOnNegativeIndex() {229 int[] array = {1, 2, 3};230 assertThatThrownBy(() -> array[-1])231 .isInstanceOf(ArrayIndexOutOfBoundsException.class);232 }233234 @Test235 void shouldThrowOnOutOfBoundsIndex() {236 int[] array = {1, 2, 3};237 assertThatThrownBy(() -> array[10])238 .isInstanceOf(ArrayIndexOutOfBoundsException.class);239 }240241 @Test242 void shouldHandleEmptyArray() {243 int[] empty = {};244 assertThat(empty.length).isZero();245 assertThatThrownBy(() -> empty[0])246 .isInstanceOf(ArrayIndexOutOfBoundsException.class);247 }248}249```250251## Best Practices252253- **Test at boundaries explicitly**: don't rely on random testing254- **Test null and empty separately** from valid inputs255- **Use parameterized tests** for multiple boundary cases256- **Test both sides of boundaries** (just below, at, just above)257- **Verify error messages** for invalid boundary inputs258- **Document why** specific boundaries matter for your domain259- **Test overflow/underflow** for all numeric operations260261## Constraints and Warnings262263- **Integer overflow**: Use `Math.addExact()` to detect silent overflow264- **Floating-point precision**: Never use exact equality; always use tolerance-based assertions265- **NaN behavior**: `NaN != NaN`; use `Float.isNaN()` or `Double.isNaN()`266- **Collection size limits**: Be mindful of memory with large test collections267- **String encoding**: Test with Unicode characters for internationalization268- **Date/time boundaries**: Account for timezone transitions and daylight saving269- **Array indexing**: Always test index 0, length-1, and out-of-bounds270271## References272273- [Integer.MIN_VALUE/MAX_VALUE](https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html)274- [Double.MIN_VALUE/MAX_VALUE](https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html)275- [AssertJ Floating Point](https://assertj.github.io/assertj-core-features-highlight.html#assertions-on-numbers)276- [Boundary Value Analysis](https://en.wikipedia.org/wiki/Boundary-value_analysis)277- [references/concurrent-testing.md](references/concurrent-testing.md) - Thread safety patterns278- [references/parameterized-patterns.md](references/parameterized-patterns.md) - Off-by-one and parameterized examples279280---281> Converted and distributed by [TomeVault](https://tomevault.io/claim/giuseppe-trisciuoglio) — claim your Tome and manage your conversions.282<!-- tomevault:4.0:skill_md:2026-04-11 -->