unit-test-boundary-conditions
giuseppe-trisciuoglio/developer-kit
Test boundary conditions, edge cases, and limits in Java with JUnit 5 and AssertJ patterns.
What is unit-test-boundary-conditions?
Provides systematic patterns for testing numeric boundaries, null/empty inputs, collection states, floating-point precision, and off-by-one errors in Java unit tests. Use when writing .java test files to ensure code handles limits, corner cases, and special inputs correctly.
- Test numeric min/max values, zero, and overflow/underflow scenarios
- Validate null, empty, and whitespace string inputs
- Test collection boundaries: empty (0), single (1), and large (many) element cases
- Verify floating-point precision with tolerance-based assertions
- Test array index boundaries and out-of-bounds access
- Handle date/time edge cases including leap years and timezone transitions
How to install unit-test-boundary-conditions
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-boundary-conditions- JUnit 5 (junit-jupiter)
- JUnit 5 Parameterized Tests (junit-jupiter-params)
- AssertJ (assertj-core)
How to use unit-test-boundary-conditions
- 1.Identify all boundaries in your code: numeric limits (MIN_VALUE, MAX_VALUE, zero), string states (null, empty, whitespace), collection sizes (0, 1, many)
- 2.Use @ParameterizedTest with @ValueSource or @CsvSource to test multiple boundary values in one test
- 3.Test both sides of each boundary: values just below, at, and just above the limit
- 4.Apply AssertJ assertions like isCloseTo(expected, within(tolerance)) for floating-point comparisons
- 5.Test collection states explicitly: empty, single element, and multiple elements
- 6.Use Math.addExact() and Math.subtractExact() to detect arithmetic overflow/underflow
- 7.Run tests after adding each boundary category to catch issues early
- 8.Iterate based on test failures: analyze errors to discover additional untested boundaries and add cases
Use cases
- Testing integer arithmetic operations for overflow/underflow using Math.addExact()
- Validating string processing functions with null, empty, and whitespace inputs
- Ensuring collection utilities handle empty lists, single elements, and large datasets
- Verifying floating-point calculations use isCloseTo() with appropriate tolerance
- Testing array access at first, last, negative, and out-of-bounds indices
- Java developers writing unit tests
- QA engineers testing numeric and collection operations
- Backend developers ensuring robust error handling at system limits
- Teams using JUnit 5 and AssertJ for test automation
unit-test-boundary-conditions FAQ
Use @ParameterizedTest with @ValueSource or @CsvSource for testing multiple similar boundary values (e.g., Integer.MIN_VALUE, 0, Integer.MAX_VALUE). Use individual @Test methods for complex assertions or when testing different error conditions.
Never use exact equality (==) for floating-point values. Always use AssertJ's isCloseTo(expected, within(tolerance)) to account for precision errors, e.g., assertThat(0.1 + 0.2).isCloseTo(0.3, within(0.0001)).
Null means the reference is absent; empty is a zero-length string ""; whitespace is strings containing only spaces, tabs, or newlines. Test all three separately since code may handle them differently.
Use Math.addExact() and Math.subtractExact() instead of regular operators. These throw ArithmeticException on overflow, which you can verify with assertThatThrownBy().
Yes. Always test empty (size 0), single element (size 1), and multiple elements (size > 1) for any collection operation. Edge cases often hide in empty or single-element scenarios.
Full instructions (SKILL.md)
Source of truth, from giuseppe-trisciuoglio/developer-kit.
name: unit-test-boundary-conditions 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
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
@ParameterizedTestwith@ValueSourceor@CsvSourcefor 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()andMath.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; useFloat.isNaN()orDouble.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
- Double.MIN_VALUE/MAX_VALUE
- AssertJ Floating Point
- Boundary Value Analysis
- references/concurrent-testing.md - Thread safety patterns
- references/parameterized-patterns.md - Off-by-one and parameterized examples
Related skills
More from giuseppe-trisciuoglio/developer-kit and the wider catalog.

unit-test-caching
Unit test patterns for Spring Cache annotations (@Cacheable, @CachePut, @CacheEvict) with mocked cache managers.

unit-test-config-properties
Unit test Spring Boot @ConfigurationProperties with property binding, validation, and type conversion patterns.

unit-test-controller-layer
Unit test REST controllers in isolation using MockMvc and @WebMvcTest patterns.

unit-test-exception-handler
Unit test patterns for Spring Boot @ExceptionHandler and @ControllerAdvice with MockMvc

unit-test-json-serialization
Unit test JSON serialization/deserialization with Spring's @JsonTest and Jackson patterns.

unit-test-mapper-converter
Unit testing patterns for MapStruct mappers and custom converters with null handling and nested object validation.