unit-test-boundary-conditions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnit 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
在以下场景使用该技巧:
- 测试最小值和最大值
- 测试空值和空输入
- 测试仅含空白字符的字符串
- 测试溢出/下溢场景
- 测试包含0个、1个或多个元素的集合
- 验证API边界处的行为
- 需要全面的边缘案例覆盖
Setup: Boundary Testing
环境搭建:边界测试
Maven
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>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
Gradle
kotlin
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("org.assertj:assertj-core")
}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);
}
}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);
}
}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);
}
}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();
}
}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);
}
}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);
}
}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();
}
}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();
}
}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
- 明确测试边界 - 不要依赖随机测试
- 将空值和空输入与有效输入分开测试
- 对多个边界案例使用参数化测试
- 测试边界的两侧(略低于、等于、略高于边界值)
- 验证错误信息对无效边界场景有帮助
- 记录特定边界的重要性原因
- 测试数值运算的溢出/下溢
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 , for clear boundaries.
LocalDate.MINLocalDate.MAX浮点数比较失败:使用方法。
isCloseTo(expected, within(tolerance))集合边界不清晰:明确列出场景:空集合(0个元素)、单元素集合(1个元素)、多元素集合(>1个元素)。
日期边界混淆:使用、来明确边界。
LocalDate.MINLocalDate.MAX