关于 unit-test-boundary-conditions
unit-test-boundary-conditions 是面向使用 JUnit 5 的 Java 开发者的测试技能,提供数值、字符串、集合、浮点数、日期时间及索引边界的系统化测试模式。
主要功能
- 01测试数值边界与溢出
- 02覆盖空值、空字符串和空白字符串
- 03检查空集合与非空集合
- 04以容差断言浮点数值
- 05测试日期、时间和数组边界
适用场景
- 01验证数值极限处的行为
- 02发现差一错误
- 03检查算术溢出与下溢
- 04测试闰年和时区转换
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill unit-test-boundary-conditions安装后在 Claude Code 中输入/unit-test-boundary-conditions确认已加载。
附带文件references/concurrent-testing.mdreferences/parameterized-patterns.md
详细介绍
单元测试边界条件和边缘情况
概述
使用 JUnit 5 测试边界条件、边界情况和极限值的系统化模式。涵盖数值边界、字符串边缘情况、集合状态、浮点精度、日期/时间极限值以及差一错误场景。
适用场景
- 数值最小值/最大值限制、空值/空字符串/空白字符输入
- 溢出/下溢验证、集合边界
- 差一错误、浮点精度
说明
- 识别边界:列出数值限制(MIN_VALUE、MAX_VALUE、零)、字符串状态(空值、空字符串、空白字符)、集合大小(0、1、多个)
- 使用参数化测试:使用
@ParameterizedTest搭配@ValueSource或@CsvSource测试多个边界值 - 测试边界两侧:覆盖每个边界正下方、边界处和正上方的值
- 每添加一类边界后运行测试,以便及早发现问题
- 验证浮点精度:使用 AssertJ 的
isCloseTo(expected, within(tolerance)) - 测试集合状态:明确测试空(0 个)、单个(1 个)和多个(>1 个)元素的场景
- 处理溢出/下溢:使用
Math.addExact()和Math.subtractExact()检测算术溢出 - 测试日期/时间边缘情况:验证闰年、月份边界和时区转换
- 根据失败迭代调整:当边界测试失败时,分析错误以发现其他尚未测试的边界;为新发现的边缘情况添加测试用例
示例
需要:junit-jupiter、junit-jupiter-params、assertj-core。
整数边界测试
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);
}
}
字符串边界测试
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();
}
}
集合边界测试
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);
}
}
浮点边界测试
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();
}
}
日期/时间边界测试
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);
}
}
数组索引边界测试
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);
}
}
最佳实践
- 明确测试边界:不要依赖随机测试
- 将空值和空字符串与有效输入分开测试
- 对多个边界情况使用参数化测试
- 测试边界两侧(正下方、边界处、正上方)
- 验证无效边界输入的错误消息
- 记录特定边界对你的领域为何重要
- 测试所有数值运算的溢出/下溢
限制和警告
- 整数溢出:使用
Math.addExact()检测静默溢出 - 浮点精度:切勿使用精确相等判断;始终使用基于容差的断言
- NaN 行为:
NaN != NaN;使用Float.isNaN()或Double.isNaN() - 集合大小限制:大型测试集合需注意内存占用
- 字符串编码:使用 Unicode 字符进行国际化测试
- 日期/时间边界:考虑时区转换和夏令时
- 数组索引:始终测试索引 0、length-1 和越界情况
参考资料
评论
支持 Markdown