unit-test-utility-methods
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnit Testing Utility Classes and Static Methods
单元测试工具类与静态方法
Overview
概述
This skill provides comprehensive patterns for testing utility classes and static methods using JUnit 5. It covers testing pure functions without side effects, edge case handling, boundary conditions, and common utility patterns like string manipulation, calculations, collections, and data validation.
本技能提供使用JUnit 5测试工具类和静态方法的全面模式,涵盖无副作用纯函数的测试、边界情况处理、边界条件,以及字符串处理、计算、集合、数据验证等常见工具模式。
When to Use
适用场景
Use this skill when:
- Testing utility classes with static helper methods
- Testing pure functions with no state or side effects
- Testing string manipulation and formatting utilities
- Testing calculation and conversion utilities
- Testing collections and array utilities
- Want simple, fast tests without mocking complexity
- Testing data transformation and validation helpers
在以下场景中使用本技能:
- 测试包含静态辅助方法的工具类
- 测试无状态、无副作用的纯函数
- 测试字符串处理与格式化工具
- 测试计算与转换工具
- 测试集合与数组工具
- 希望编写无需复杂Mock的简单、快速测试
- 测试数据转换与验证辅助方法
Instructions
操作步骤
Follow these steps to test utility classes and static methods:
按照以下步骤测试工具类与静态方法:
1. Create Test Class
1. 创建测试类
Create a JUnit 5 test class named after the utility class being tested (e.g., StringUtilsTest).
创建与待测试工具类同名的JUnit 5测试类(例如:StringUtilsTest)。
2. Test Happy Path
2. 测试正常路径
Write tests for typical use cases with valid inputs to verify correct behavior.
编写针对典型有效输入用例的测试,验证正确行为。
3. Test Edge Cases
3. 测试边界情况
Test null inputs, empty strings, zero values, and boundary conditions.
测试空输入、空字符串、零值及边界条件。
4. Test Error Cases
4. 测试错误场景
Verify proper exception throwing for invalid inputs when applicable.
适当时,验证无效输入下是否正确抛出异常。
5. Use Descriptive Test Names
5. 使用描述性测试名称
Name tests to clearly indicate what scenario is being tested (e.g., shouldCapitalizeFirstLetter).
为测试命名以清晰表明所测试的场景(例如:shouldCapitalizeFirstLetter)。
6. Use AssertJ Assertions
6. 使用AssertJ断言
Leverage AssertJ's readable assertion methods for clear test code.
利用AssertJ可读性强的断言方法编写清晰的测试代码。
7. Consider Parameterized Tests
7. 考虑参数化测试
Use @ParameterizedTest for testing multiple similar scenarios with different inputs.
使用@ParameterizedTest测试多个相似场景的不同输入。
Examples
示例
Basic Pattern: Static Utility Testing
基础模式:静态工具测试
Simple String Utility
简单字符串工具
java
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class StringUtilsTest {
@Test
void shouldCapitalizeFirstLetter() {
String result = StringUtils.capitalize("hello");
assertThat(result).isEqualTo("Hello");
}
@Test
void shouldHandleEmptyString() {
String result = StringUtils.capitalize("");
assertThat(result).isEmpty();
}
@Test
void shouldHandleNullInput() {
String result = StringUtils.capitalize(null);
assertThat(result).isNull();
}
@Test
void shouldHandleSingleCharacter() {
String result = StringUtils.capitalize("a");
assertThat(result).isEqualTo("A");
}
@Test
void shouldNotChangePascalCase() {
String result = StringUtils.capitalize("Hello");
assertThat(result).isEqualTo("Hello");
}
}java
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class StringUtilsTest {
@Test
void shouldCapitalizeFirstLetter() {
String result = StringUtils.capitalize("hello");
assertThat(result).isEqualTo("Hello");
}
@Test
void shouldHandleEmptyString() {
String result = StringUtils.capitalize("");
assertThat(result).isEmpty();
}
@Test
void shouldHandleNullInput() {
String result = StringUtils.capitalize(null);
assertThat(result).isNull();
}
@Test
void shouldHandleSingleCharacter() {
String result = StringUtils.capitalize("a");
assertThat(result).isEqualTo("A");
}
@Test
void shouldNotChangePascalCase() {
String result = StringUtils.capitalize("Hello");
assertThat(result).isEqualTo("Hello");
}
}Testing Null Handling
测试空值处理
Null-Safe Utility Methods
空值安全工具方法
java
class NullSafeUtilsTest {
@Test
void shouldReturnDefaultValueWhenNull() {
Object result = NullSafeUtils.getOrDefault(null, "default");
assertThat(result).isEqualTo("default");
}
@Test
void shouldReturnValueWhenNotNull() {
Object result = NullSafeUtils.getOrDefault("value", "default");
assertThat(result).isEqualTo("value");
}
@Test
void shouldReturnFalseWhenStringIsNull() {
boolean result = NullSafeUtils.isNotBlank(null);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrueWhenStringHasContent() {
boolean result = NullSafeUtils.isNotBlank(" text ");
assertThat(result).isTrue();
}
}java
class NullSafeUtilsTest {
@Test
void shouldReturnDefaultValueWhenNull() {
Object result = NullSafeUtils.getOrDefault(null, "default");
assertThat(result).isEqualTo("default");
}
@Test
void shouldReturnValueWhenNotNull() {
Object result = NullSafeUtils.getOrDefault("value", "default");
assertThat(result).isEqualTo("value");
}
@Test
void shouldReturnFalseWhenStringIsNull() {
boolean result = NullSafeUtils.isNotBlank(null);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrueWhenStringHasContent() {
boolean result = NullSafeUtils.isNotBlank(" text ");
assertThat(result).isTrue();
}
}Testing Calculations and Conversions
测试计算与转换
Math Utilities
数学工具
java
class MathUtilsTest {
@Test
void shouldCalculatePercentage() {
double result = MathUtils.percentage(25, 100);
assertThat(result).isEqualTo(25.0);
}
@Test
void shouldHandleZeroDivisor() {
double result = MathUtils.percentage(50, 0);
assertThat(result).isZero();
}
@Test
void shouldRoundToTwoDecimalPlaces() {
double result = MathUtils.round(3.14159, 2);
assertThat(result).isEqualTo(3.14);
}
@Test
void shouldHandleNegativeNumbers() {
int result = MathUtils.absoluteValue(-42);
assertThat(result).isEqualTo(42);
}
}java
class MathUtilsTest {
@Test
void shouldCalculatePercentage() {
double result = MathUtils.percentage(25, 100);
assertThat(result).isEqualTo(25.0);
}
@Test
void shouldHandleZeroDivisor() {
double result = MathUtils.percentage(50, 0);
assertThat(result).isZero();
}
@Test
void shouldRoundToTwoDecimalPlaces() {
double result = MathUtils.round(3.14159, 2);
assertThat(result).isEqualTo(3.14);
}
@Test
void shouldHandleNegativeNumbers() {
int result = MathUtils.absoluteValue(-42);
assertThat(result).isEqualTo(42);
}
}Testing Collection Utilities
测试集合工具
List/Set/Map Operations
列表/集合/映射操作
java
class CollectionUtilsTest {
@Test
void shouldFilterList() {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> evenNumbers = CollectionUtils.filter(numbers, n -> n % 2 == 0);
assertThat(evenNumbers).containsExactly(2, 4);
}
@Test
void shouldReturnEmptyListWhenNoMatches() {
List<Integer> numbers = List.of(1, 3, 5);
List<Integer> evenNumbers = CollectionUtils.filter(numbers, n -> n % 2 == 0);
assertThat(evenNumbers).isEmpty();
}
@Test
void shouldHandleNullList() {
List<Integer> result = CollectionUtils.filter(null, n -> true);
assertThat(result).isEmpty();
}
@Test
void shouldJoinStringsWithSeparator() {
String result = CollectionUtils.join(List.of("a", "b", "c"), "-");
assertThat(result).isEqualTo("a-b-c");
}
@Test
void shouldHandleEmptyList() {
String result = CollectionUtils.join(List.of(), "-");
assertThat(result).isEmpty();
}
@Test
void shouldDeduplicateList() {
List<String> input = List.of("apple", "banana", "apple", "cherry", "banana");
Set<String> unique = CollectionUtils.deduplicate(input);
assertThat(unique).containsExactlyInAnyOrder("apple", "banana", "cherry");
}
}java
class CollectionUtilsTest {
@Test
void shouldFilterList() {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> evenNumbers = CollectionUtils.filter(numbers, n -> n % 2 == 0);
assertThat(evenNumbers).containsExactly(2, 4);
}
@Test
void shouldReturnEmptyListWhenNoMatches() {
List<Integer> numbers = List.of(1, 3, 5);
List<Integer> evenNumbers = CollectionUtils.filter(numbers, n -> n % 2 == 0);
assertThat(evenNumbers).isEmpty();
}
@Test
void shouldHandleNullList() {
List<Integer> result = CollectionUtils.filter(null, n -> true);
assertThat(result).isEmpty();
}
@Test
void shouldJoinStringsWithSeparator() {
String result = CollectionUtils.join(List.of("a", "b", "c"), "-");
assertThat(result).isEqualTo("a-b-c");
}
@Test
void shouldHandleEmptyList() {
String result = CollectionUtils.join(List.of(), "-");
assertThat(result).isEmpty();
}
@Test
void shouldDeduplicateList() {
List<String> input = List.of("apple", "banana", "apple", "cherry", "banana");
Set<String> unique = CollectionUtils.deduplicate(input);
assertThat(unique).containsExactlyInAnyOrder("apple", "banana", "cherry");
}
}Testing String Transformations
测试字符串转换
Format and Parse Utilities
格式化与解析工具
java
class FormatUtilsTest {
@Test
void shouldFormatCurrencyWithSymbol() {
String result = FormatUtils.formatCurrency(1234.56);
assertThat(result).isEqualTo("$1,234.56");
}
@Test
void shouldHandleNegativeCurrency() {
String result = FormatUtils.formatCurrency(-100.00);
assertThat(result).isEqualTo("-$100.00");
}
@Test
void shouldParsePhoneNumber() {
String result = FormatUtils.parsePhoneNumber("5551234567");
assertThat(result).isEqualTo("(555) 123-4567");
}
@Test
void shouldFormatDate() {
LocalDate date = LocalDate.of(2024, 1, 15);
String result = FormatUtils.formatDate(date, "yyyy-MM-dd");
assertThat(result).isEqualTo("2024-01-15");
}
@Test
void shouldSluggifyString() {
String result = FormatUtils.sluggify("Hello World! 123");
assertThat(result).isEqualTo("hello-world-123");
}
}java
class FormatUtilsTest {
@Test
void shouldFormatCurrencyWithSymbol() {
String result = FormatUtils.formatCurrency(1234.56);
assertThat(result).isEqualTo("$1,234.56");
}
@Test
void shouldHandleNegativeCurrency() {
String result = FormatUtils.formatCurrency(-100.00);
assertThat(result).isEqualTo("-$100.00");
}
@Test
void shouldParsePhoneNumber() {
String result = FormatUtils.parsePhoneNumber("5551234567");
assertThat(result).isEqualTo("(555) 123-4567");
}
@Test
void shouldFormatDate() {
LocalDate date = LocalDate.of(2024, 1, 15);
String result = FormatUtils.formatDate(date, "yyyy-MM-dd");
assertThat(result).isEqualTo("2024-01-15");
}
@Test
void shouldSluggifyString() {
String result = FormatUtils.sluggify("Hello World! 123");
assertThat(result).isEqualTo("hello-world-123");
}
}Testing Data Validation
测试数据验证
Validator Utilities
验证器工具
java
class ValidatorUtilsTest {
@Test
void shouldValidateEmailFormat() {
boolean valid = ValidatorUtils.isValidEmail("user@example.com");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidEmail("invalid-email");
assertThat(invalid).isFalse();
}
@Test
void shouldValidatePhoneNumber() {
boolean valid = ValidatorUtils.isValidPhone("555-123-4567");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidPhone("12345");
assertThat(invalid).isFalse();
}
@Test
void shouldValidateUrlFormat() {
boolean valid = ValidatorUtils.isValidUrl("https://example.com");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidUrl("not a url");
assertThat(invalid).isFalse();
}
@Test
void shouldValidateCreditCardNumber() {
boolean valid = ValidatorUtils.isValidCreditCard("4532015112830366");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidCreditCard("1234567890123456");
assertThat(invalid).isFalse();
}
}java
class ValidatorUtilsTest {
@Test
void shouldValidateEmailFormat() {
boolean valid = ValidatorUtils.isValidEmail("user@example.com");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidEmail("invalid-email");
assertThat(invalid).isFalse();
}
@Test
void shouldValidatePhoneNumber() {
boolean valid = ValidatorUtils.isValidPhone("555-123-4567");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidPhone("12345");
assertThat(invalid).isFalse();
}
@Test
void shouldValidateUrlFormat() {
boolean valid = ValidatorUtils.isValidUrl("https://example.com");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidUrl("not a url");
assertThat(invalid).isFalse();
}
@Test
void shouldValidateCreditCardNumber() {
boolean valid = ValidatorUtils.isValidCreditCard("4532015112830366");
assertThat(valid).isTrue();
boolean invalid = ValidatorUtils.isValidCreditCard("1234567890123456");
assertThat(invalid).isFalse();
}
}Testing Parameterized Scenarios
测试参数化场景
Multiple Test Cases with @ParameterizedTest
使用@ParameterizedTest的多测试用例
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;
class StringUtilsParametrizedTest {
@ParameterizedTest
@ValueSource(strings = {"", " ", "null", "undefined"})
void shouldConsiderFalsyValuesAsEmpty(String input) {
boolean result = StringUtils.isEmpty(input);
assertThat(result).isTrue();
}
@ParameterizedTest
@CsvSource({
"hello,HELLO",
"world,WORLD",
"javaScript,JAVASCRIPT",
"123ABC,123ABC"
})
void shouldConvertToUpperCase(String input, String expected) {
String result = StringUtils.toUpperCase(input);
assertThat(result).isEqualTo(expected);
}
}java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;
class StringUtilsParametrizedTest {
@ParameterizedTest
@ValueSource(strings = {"", " ", "null", "undefined"})
void shouldConsiderFalsyValuesAsEmpty(String input) {
boolean result = StringUtils.isEmpty(input);
assertThat(result).isTrue();
}
@ParameterizedTest
@CsvSource({
"hello,HELLO",
"world,WORLD",
"javaScript,JAVASCRIPT",
"123ABC,123ABC"
})
void shouldConvertToUpperCase(String input, String expected) {
String result = StringUtils.toUpperCase(input);
assertThat(result).isEqualTo(expected);
}
}Testing with Mockito for External Dependencies
使用Mockito测试外部依赖
Utility with Dependency (Rare Case)
含依赖的工具类(罕见场景)
java
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DateUtilsTest {
@Mock
private Clock clock;
@Test
void shouldGetCurrentDateFromClock() {
Instant fixedTime = Instant.parse("2024-01-15T10:30:00Z");
when(clock.instant()).thenReturn(fixedTime);
LocalDate result = DateUtils.today(clock);
assertThat(result).isEqualTo(LocalDate.of(2024, 1, 15));
}
}java
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DateUtilsTest {
@Mock
private Clock clock;
@Test
void shouldGetCurrentDateFromClock() {
Instant fixedTime = Instant.parse("2024-01-15T10:30:00Z");
when(clock.instant()).thenReturn(fixedTime);
LocalDate result = DateUtils.today(clock);
assertThat(result).isEqualTo(LocalDate.of(2024, 1, 15));
}
}Edge Cases and Boundary Testing
边界情况与极限测试
java
class MathUtilsEdgeCaseTest {
@Test
void shouldHandleMaxIntegerValue() {
int result = MathUtils.increment(Integer.MAX_VALUE);
assertThat(result).isEqualTo(Integer.MAX_VALUE);
}
@Test
void shouldHandleMinIntegerValue() {
int result = MathUtils.decrement(Integer.MIN_VALUE);
assertThat(result).isEqualTo(Integer.MIN_VALUE);
}
@Test
void shouldHandleVeryLargeNumbers() {
BigDecimal result = MathUtils.add(
new BigDecimal("999999999999.99"),
new BigDecimal("0.01")
);
assertThat(result).isEqualTo(new BigDecimal("1000000000000.00"));
}
@Test
void shouldHandleFloatingPointPrecision() {
double result = MathUtils.multiply(0.1, 0.2);
assertThat(result).isCloseTo(0.02, within(0.0001));
}
}java
class MathUtilsEdgeCaseTest {
@Test
void shouldHandleMaxIntegerValue() {
int result = MathUtils.increment(Integer.MAX_VALUE);
assertThat(result).isEqualTo(Integer.MAX_VALUE);
}
@Test
void shouldHandleMinIntegerValue() {
int result = MathUtils.decrement(Integer.MIN_VALUE);
assertThat(result).isEqualTo(Integer.MIN_VALUE);
}
@Test
void shouldHandleVeryLargeNumbers() {
BigDecimal result = MathUtils.add(
new BigDecimal("999999999999.99"),
new BigDecimal("0.01")
);
assertThat(result).isEqualTo(new BigDecimal("1000000000000.00"));
}
@Test
void shouldHandleFloatingPointPrecision() {
double result = MathUtils.multiply(0.1, 0.2);
assertThat(result).isCloseTo(0.02, within(0.0001));
}
}Best Practices
最佳实践
- Test pure functions exclusively - no side effects or state
- Cover happy path and edge cases - null, empty, extreme values
- Use descriptive test names - clearly state what's being tested
- Keep tests simple and short - utility tests should be quick to understand
- Use @ParameterizedTest for testing multiple similar scenarios
- Avoid mocking when not needed - only mock external dependencies
- Test boundary conditions - min/max values, empty collections, null inputs
- 仅测试纯函数 - 无副作用或状态
- 覆盖正常路径与边界情况 - 空值、空集合、极值
- 使用描述性测试名称 - 清晰说明测试内容
- 保持测试简洁短小 - 工具类测试应易于理解
- 使用@ParameterizedTest 测试多个相似场景
- 无需时避免Mock - 仅对外部依赖进行Mock
- 测试边界条件 - 最大/最小值、空集合、空输入
Common Pitfalls
常见陷阱
- Testing framework behavior instead of utility logic
- Over-mocking when pure functions need no mocks
- Not testing null/empty edge cases
- Not testing negative numbers and extreme values
- Test methods too large - split complex scenarios
- 测试框架行为而非工具逻辑
- 纯函数无需Mock时过度Mock
- 未测试空值/空边界情况
- 未测试负数与极值
- 测试方法过大 - 拆分复杂场景
Constraints and Warnings
约束与注意事项
- Static methods cannot be mocked: Use reflection-based utilities like PowerMock only when absolutely necessary
- Pure function requirement: Utility methods should be pure functions; testing stateful utilities is difficult
- Floating point precision: Never use exact equality for floating point comparisons; use tolerance
- Null handling consistency: Decide whether utilities return null or throw exceptions for invalid input
- Immutable inputs: Document clearly whether utility methods modify input parameters
- Thread safety: Static utilities must be thread-safe; verify under concurrent access
- External dependencies: Minimize external dependencies in utility classes for easier testing
- 静态方法无法被Mock:仅在绝对必要时使用PowerMock等基于反射的工具
- 纯函数要求:工具方法应是纯函数;有状态工具类的测试难度大
- 浮点精度:浮点比较绝不能使用精确相等,需使用容差
- 空值处理一致性:明确工具类对无效输入是返回空值还是抛出异常
- 不可变输入:明确说明工具方法是否修改输入参数
- 线程安全:静态工具类必须线程安全;需在并发访问下验证
- 外部依赖:尽量减少工具类的外部依赖以简化测试
Examples
示例
Input: Utility Method Without Tests
输入:无测试的工具方法
java
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
}java
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
}Output: Complete Test Coverage
输出:完整测试覆盖
java
class StringUtilsTest {
@Test
void shouldReturnTrueForNullString() {
assertThat(StringUtils.isEmpty(null)).isTrue();
}
@Test
void shouldReturnTrueForEmptyString() {
assertThat(StringUtils.isEmpty("")).isTrue();
}
@Test
void shouldReturnTrueForWhitespaceOnly() {
assertThat(StringUtils.isEmpty(" ")).isTrue();
}
@Test
void shouldReturnFalseForNonEmptyString() {
assertThat(StringUtils.isEmpty("hello")).isFalse();
}
}java
class StringUtilsTest {
@Test
void shouldReturnTrueForNullString() {
assertThat(StringUtils.isEmpty(null)).isTrue();
}
@Test
void shouldReturnTrueForEmptyString() {
assertThat(StringUtils.isEmpty("")).isTrue();
}
@Test
void shouldReturnTrueForWhitespaceOnly() {
assertThat(StringUtils.isEmpty(" ")).isTrue();
}
@Test
void shouldReturnFalseForNonEmptyString() {
assertThat(StringUtils.isEmpty("hello")).isFalse();
}
}Input: Calculation Without Edge Case Testing
输入:无边界测试的计算方法
java
public static double percentage(int value, int total) {
return (value / total) * 100.0;
}java
public static double percentage(int value, int total) {
return (value / total) * 100.0;
}Output: Tests Covering Edge Cases
输出:覆盖边界情况的测试
java
class MathUtilsTest {
@Test
void shouldCalculateNormalPercentage() {
assertThat(MathUtils.percentage(25, 100)).isEqualTo(25.0);
}
@Test
void shouldHandleZeroDivisor() {
assertThat(MathUtils.percentage(50, 0)).isEqualTo(0.0);
}
@Test
void shouldHandleZeroValue() {
assertThat(MathUtils.percentage(0, 100)).isEqualTo(0.0);
}
}java
class MathUtilsTest {
@Test
void shouldCalculateNormalPercentage() {
assertThat(MathUtils.percentage(25, 100)).isEqualTo(25.0);
}
@Test
void shouldHandleZeroDivisor() {
assertThat(MathUtils.percentage(50, 0)).isEqualTo(0.0);
}
@Test
void shouldHandleZeroValue() {
assertThat(MathUtils.percentage(0, 100)).isEqualTo(0.0);
}
}Constraints and Warnings
约束与注意事项
Floating point precision issues: Use with delta instead of exact equality.
isCloseTo()Null handling inconsistency: Decide whether utility returns null or throws exception, then test consistently.
Complex utility logic belongs elsewhere: Consider refactoring into testable units.
浮点精度问题:使用并设置容差,而非精确相等。
isCloseTo()空值处理不一致:明确工具类对无效输入是返回空值还是抛出异常,然后保持一致的测试。
复杂工具逻辑应拆分:考虑将复杂逻辑重构为可测试的单元。