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
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---5
6# Unit Testing Boundary Conditions and Edge Cases
7
8## Overview
9
10Systematic 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.
11
12## When to Use
13
14- Numeric min/max limits, null/empty/whitespace inputs
15- Overflow/underflow validation, collection boundaries
16- Off-by-one errors, floating-point precision
17
18## Instructions
19
201. **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 values
223. **Test both sides of boundaries**: Cover values just below, at, and just above each boundary
234. **Run tests after adding each boundary category** to catch issues early
245. **Verify floating-point precision**: Use `isCloseTo(expected, within(tolerance))` with AssertJ
256. **Test collection states**: Explicitly test empty (0), single (1), and many (>1) element scenarios
267. **Handle overflow/underflow**: Use `Math.addExact()` and `Math.subtractExact()` to detect arithmetic overflow
278. **Test date/time edges**: Verify leap years, month boundaries, timezone transitions
289. **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
29
30## Examples
31
32Requires: `junit-jupiter`, `junit-jupiter-params`, `assertj-core`.
33
34## Integer Boundary Testing
35
36```java
37import org.junit.jupiter.params.ParameterizedTest;
38import org.junit.jupiter.params.provider.ValueSource;
39import static org.assertj.core.api.Assertions.*;
40
41class IntegerBoundaryTest {
42
43 @ParameterizedTest
44 @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 }
48
49 @Test
50 void shouldDetectIntegerOverflow() {
51 assertThatThrownBy(() -> Math.addExact(Integer.MAX_VALUE, 1))
52 .isInstanceOf(ArithmeticException.class);
53 }
54
55 @Test
56 void shouldDetectIntegerUnderflow() {
57 assertThatThrownBy(() -> Math.subtractExact(Integer.MIN_VALUE, 1))
58 .isInstanceOf(ArithmeticException.class);
59 }
60
61 @Test
62 void shouldHandleZeroEdge() {
63 int result = MathUtils.divide(0, 5);
64 assertThat(result).isZero();
65
66 assertThatThrownBy(() -> MathUtils.divide(5, 0))
67 .isInstanceOf(ArithmeticException.class);
68 }
69}
70```
71
72## String Boundary Testing
73
74```java
75import org.junit.jupiter.params.ParameterizedTest;
76import org.junit.jupiter.params.provider.ValueSource;
77
78class StringBoundaryTest {
79
80 @ParameterizedTest
81 @ValueSource(strings = {"", " ", " ", "\t", "\n"})
82 void shouldRejectEmptyAndWhitespace(String input) {
83 boolean result = StringUtils.isNotBlank(input);
84 assertThat(result).isFalse();
85 }
86
87 @Test
88 void shouldHandleNullString() {
89 String result = StringUtils.trim(null);
90 assertThat(result).isNull();
91 }
92
93 @Test
94 void shouldHandleSingleCharacter() {
95 assertThat(StringUtils.capitalize("a")).isEqualTo("A");
96 assertThat(StringUtils.trim("x")).isEqualTo("x");
97 }
98
99 @Test
100 void shouldHandleVeryLongString() {
101 String longString = "x".repeat(1000000);
102
103 assertThat(longString.length()).isEqualTo(1000000);
104 assertThat(StringUtils.isNotBlank(longString)).isTrue();
105 }
106}
107```
108
109## Collection Boundary Testing
110
111```java
112class CollectionBoundaryTest {
113
114 @Test
115 void shouldHandleEmptyList() {
116 List<String> empty = List.of();
117
118 assertThat(empty).isEmpty();
119 assertThat(CollectionUtils.first(empty)).isNull();
120 assertThat(CollectionUtils.count(empty)).isZero();
121 }
122
123 @Test
124 void shouldHandleSingleElementList() {
125 List<String> single = List.of("only");
126
127 assertThat(single).hasSize(1);
128 assertThat(CollectionUtils.first(single)).isEqualTo("only");
129 assertThat(CollectionUtils.last(single)).isEqualTo("only");
130 }
131
132 @Test
133 void shouldHandleLargeList() {
134 List<Integer> large = new ArrayList<>();
135 for (int i = 0; i < 100000; i++) {
136 large.add(i);
137 }
138
139 assertThat(large).hasSize(100000);
140 assertThat(CollectionUtils.first(large)).isZero();
141 assertThat(CollectionUtils.last(large)).isEqualTo(99999);
142 }
143
144 @Test
145 void shouldHandleNullInCollection() {
146 List<String> withNull = new ArrayList<>(List.of("a", null, "c"));
147
148 assertThat(withNull).contains(null);
149 assertThat(CollectionUtils.filterNonNull(withNull)).hasSize(2);
150 }
151}
152```
153
154## Floating-Point Boundary Testing
155
156```java
157class FloatingPointBoundaryTest {
158
159 @Test
160 void shouldHandleFloatingPointPrecision() {
161 double result = 0.1 + 0.2;
162 assertThat(result).isCloseTo(0.3, within(0.0001));
163 }
164
165 @Test
166 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 }
171
172 @Test
173 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```
180
181## Date/Time Boundary Testing
182
183```java
184class DateTimeBoundaryTest {
185
186 @Test
187 void shouldHandleMinAndMaxDates() {
188 LocalDate min = LocalDate.MIN;
189 LocalDate max = LocalDate.MAX;
190
191 assertThat(min).isBefore(max);
192 assertThat(DateUtils.isValid(min)).isTrue();
193 assertThat(DateUtils.isValid(max)).isTrue();
194 }
195
196 @Test
197 void shouldHandleLeapYearBoundary() {
198 LocalDate leapYearEnd = LocalDate.of(2024, 2, 29);
199 assertThat(leapYearEnd).isNotNull();
200 }
201
202 @Test
203 void shouldRejectInvalidDateInNonLeapYear() {
204 assertThatThrownBy(() -> LocalDate.of(2023, 2, 29))
205 .isInstanceOf(DateTimeException.class);
206 }
207}
208```
209
210## Array Index Boundary Testing
211
212```java
213class ArrayBoundaryTest {
214
215 @Test
216 void shouldHandleFirstElementAccess() {
217 int[] array = {1, 2, 3, 4, 5};
218 assertThat(array[0]).isEqualTo(1);
219 }
220
221 @Test
222 void shouldHandleLastElementAccess() {
223 int[] array = {1, 2, 3, 4, 5};
224 assertThat(array[array.length - 1]).isEqualTo(5);
225 }
226
227 @Test
228 void shouldThrowOnNegativeIndex() {
229 int[] array = {1, 2, 3};
230 assertThatThrownBy(() -> array[-1])
231 .isInstanceOf(ArrayIndexOutOfBoundsException.class);
232 }
233
234 @Test
235 void shouldThrowOnOutOfBoundsIndex() {
236 int[] array = {1, 2, 3};
237 assertThatThrownBy(() -> array[10])
238 .isInstanceOf(ArrayIndexOutOfBoundsException.class);
239 }
240
241 @Test
242 void shouldHandleEmptyArray() {
243 int[] empty = {};
244 assertThat(empty.length).isZero();
245 assertThatThrownBy(() -> empty[0])
246 .isInstanceOf(ArrayIndexOutOfBoundsException.class);
247 }
248}
249```
250
251## Best Practices
252
253- **Test at boundaries explicitly**: don't rely on random testing
254- **Test null and empty separately** from valid inputs
255- **Use parameterized tests** for multiple boundary cases
256- **Test both sides of boundaries** (just below, at, just above)
257- **Verify error messages** for invalid boundary inputs
258- **Document why** specific boundaries matter for your domain
259- **Test overflow/underflow** for all numeric operations
260
261## Constraints and Warnings
262
263- **Integer overflow**: Use `Math.addExact()` to detect silent overflow
264- **Floating-point precision**: Never use exact equality; always use tolerance-based assertions
265- **NaN behavior**: `NaN != NaN`; use `Float.isNaN()` or `Double.isNaN()`
266- **Collection size limits**: Be mindful of memory with large test collections
267- **String encoding**: Test with Unicode characters for internationalization
268- **Date/time boundaries**: Account for timezone transitions and daylight saving
269- **Array indexing**: Always test index 0, length-1, and out-of-bounds
270
271## References
272
273- [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 patterns
278- [references/parameterized-patterns.md](references/parameterized-patterns.md) - Off-by-one and parameterized examples