Back to Testing & Quality Assurance

unit-test-boundary-conditions

unit testingboundary testingedge casesJUnitJavatest automationcode qualityrobustness
282📄 MIT🕒 2026-06-15Source ↗

Install this skill

npx skills add giuseppe-trisciuoglio/developer-kit

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

This skill provides a structured framework for verifying software stability at extreme operating points. By focusing on non-standard inputs, the approach targets failure modes that frequently escape standard happy-path testing. The process involves systematically injecting boundary values for numeric types, collections, and character sequences. It emphasizes validating system responses to integer overflow, underflow, empty data structures, and null references. Utilizing JUnit 5 and AssertJ, developers can convert subjective edge-case concerns into verifiable code requirements. The methodology reduces production defects by enforcing explicit behavior specifications for scenarios like zero-division or whitespace-heavy inputs, ensuring the codebase maintains integrity when exposed to unexpected or malicious input parameters during execution.

When to Use This Skill

  • Validating input sanitization logic for user-submitted text fields.
  • Testing calculation modules to ensure numeric precision and overflow handling.
  • Verifying data processing functions against empty or null collection inputs.
  • Checking API endpoint parameter constraints at their defined limits.

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • Test my code for edge cases.
  • Add boundary condition tests for this method.
  • Check for integer overflow bugs.
  • Write tests for empty and null list inputs.
  • Verify numeric limits on this function.

Pro Tips

  • 💡Leverage JUnit 5's `@ParameterizedTest` with `@ValueSource` or `@CsvSource` to efficiently test multiple boundary values for a single scenario.
  • 💡Combine boundary condition testing with error handling assertions (e.g., `assertThrows`) to confirm expected exceptions are thrown for invalid or out-of-bounds inputs.
  • 💡Prioritize testing boundaries where business logic or system behavior changes, such as discount tiers, data pagination limits, or access control thresholds.

What this skill does

  • Validates integer behavior at MIN_VALUE and MAX_VALUE extremes.
  • Enforces consistent handling of null, empty, and whitespace-only strings.
  • Tests collection integrity with zero, single, and massive item counts.
  • Verifies arithmetic operations against overflow and underflow errors.
  • Automates boundary verification using parameterized test patterns.

When not to use it

  • When performing high-level end-to-end integration or UI flow testing.
  • When validating complex business logic that requires stateful environment mocking.
  • When focusing on performance profiling or load testing.

Example workflow

  1. Identify critical function inputs susceptible to extreme values.
  2. Define a parameterized test suite for relevant boundary ranges.
  3. Implement AssertJ checks to confirm expected exceptions or output.
  4. Run tests to isolate arithmetic or null-pointer failures.
  5. Refactor code to handle identified edge cases gracefully.

Prerequisites

  • JUnit 5 (Jupiter)
  • AssertJ library
  • Basic knowledge of unit testing frameworks

Pitfalls & limitations

  • !Over-testing trivial boundaries that have no impact on application logic.
  • !Failing to account for language-specific integer overflow behaviors.
  • !Neglecting custom object states in collection testing.

FAQ

Why use parameterized tests for boundary conditions?
Parameterized tests allow you to run the same validation logic against multiple inputs—like MIN_VALUE and MAX_VALUE—without duplicating code.
Should I test every single possible integer value?
No, focus on significant values like zero, boundaries, and values surrounding those limits rather than brute-forcing every possible integer.
Is this approach limited to numeric data?
No, this skill applies to any input type, including strings, collections, and custom objects, where there are defined empty, null, or maximum size states.

How it compares

This skill automates specific edge-case coverage using established testing patterns, which is more reliable and reproducible than performing ad-hoc manual input testing.

Source & trust

282 stars📄 MIT🕒 Updated 2026-06-15
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
# Unit Testing Boundary Conditions and Edge Cases

Test boundary conditions, edge cases, and limit values systematically. Verify code behavior at limits, with null/empty inputs, and overflow scenarios.

## When to Use This Skill

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

## Setup: Boundary Testing

### Maven
<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
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("org.assertj:assertj-core")
}


## Numeric Boundary Testing

### Integer Limits

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

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

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

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

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

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

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

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

## 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)

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/unit-test-boundary-conditions/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-boundary-conditions/SKILL.md
  • Cursor: ~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-boundary-conditions/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-boundary-conditions/SKILL.md

🚀 Install with CLI:
npx skills add giuseppe-trisciuoglio/developer-kit

Read the Master Guide: Mastering Agent Skills

Related Skill Units

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid testing & quality assurance issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Testing & Quality Assurance and is published by Giuseppe Trisciuoglio, maintained in giuseppe-trisciuoglio/developer-kit.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.