libphonenumber单元测试编写:Mock对象与测试数据生成
引言
在现代软件开发中,电话号码处理是一个看似简单但实际复杂的任务。不同国家/地区的电话号码格式、验证规则、显示格式都存在巨大差异。Google的libphonenumber库为这一难题提供了优雅的解决方案,但如何确保这个强大库的正确性呢?答案就是完善的单元测试体系。
本文将深入探讨libphonenumber的单元测试编写实践,重点讲解Mock对象的使用和测试数据生成策略,帮助开发者构建健壮的电话号码处理功能测试。
测试环境搭建
依赖配置
libphonenumber使用Maven进行依赖管理,测试相关依赖配置如下:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
</dependencies>
测试基类设计
libphonenumber设计了专门的测试基类TestMetadataTestCase,为所有测试提供统一的测试元数据环境:
public class TestMetadataTestCase extends TestCase {
private static final String TEST_METADATA_FILE_PREFIX =
"/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProtoForTesting";
protected final PhoneNumberUtil phoneUtil;
public TestMetadataTestCase() {
phoneUtil = new PhoneNumberUtil(
new MetadataSourceImpl(new MultiFileModeFileNameProvider(TEST_METADATA_FILE_PREFIX),
DefaultMetadataDependenciesProvider.getInstance().getMetadataLoader(),
DefaultMetadataDependenciesProvider.getInstance().getMetadataParser()),
CountryCodeToRegionCodeMapForTesting.getCountryCodeToRegionCodeMap());
}
@Override
protected void setUp() throws Exception {
super.setUp();
PhoneNumberUtil.setInstance(phoneUtil);
}
@Override
protected void tearDown() throws Exception {
PhoneNumberUtil.setInstance(null);
super.tearDown();
}
}
Mock对象的使用实践
1. 元数据源Mocking
在测试中,我们经常需要模拟不同的元数据场景。libphonenumber使用Mockito来创建模拟的元数据源:
public class PhoneNumberUtilTest extends TestMetadataTestCase {
private final MetadataSource mockedMetadataSource = Mockito.mock(MetadataSource.class);
private final PhoneNumberUtil phoneNumberUtilWithMissingMetadata =
new PhoneNumberUtil(mockedMetadataSource,
CountryCodeToRegionCodeMapForTesting.getCountryCodeToRegionCodeMap());
public void testGetInstanceLoadBadMetadata() {
assertNull(phoneUtil.getMetadataForRegion("No Such Region"));
assertNull(phoneUtil.getMetadataForNonGeographicalRegion(-1));
}
}
2. 元数据加载器Mocking
对于异步加载场景,libphonenumber提供了完整的Mock示例:
public class BlockingMetadataBootstrappingGuardTest extends TestCase {
private final MetadataLoader metadataLoader = Mockito.mock(MetadataLoader.class);
private final MetadataContainer metadataContainer = Mockito.mock(MetadataContainer.class);
public void test_getOrBootstrap_shouldInvokeBootstrappingOnlyOnce() {
when(metadataLoader.loadMetadata(PHONE_METADATA_FILE))
.thenReturn(PhoneMetadataCollectionUtil.toInputStream(PHONE_METADATA));
bootstrappingGuard.getOrBootstrap(PHONE_METADATA_FILE);
bootstrappingGuard.getOrBootstrap(PHONE_METADATA_FILE);
verify(metadataLoader, times(1)).loadMetadata(PHONE_METADATA_FILE);
}
}
测试数据生成策略
1. 静态测试电话号码
libphonenumber采用静态常量方式定义测试电话号码,覆盖各种场景:
public class PhoneNumberUtilTest extends TestMetadataTestCase {
// 国际号码
private static final PhoneNumber INTERNATIONAL_TOLL_FREE =
new PhoneNumber().setCountryCode(800).setNationalNumber(12345678L);
// 美国号码
private static final PhoneNumber US_NUMBER =
new PhoneNumber().setCountryCode(1).setNationalNumber(6502530000L);
// 德国号码
private static final PhoneNumber DE_NUMBER =
new PhoneNumber().setCountryCode(49).setNationalNumber(30123456L);
// 移动号码
private static final PhoneNumber AR_MOBILE =
new PhoneNumber().setCountryCode(54).setNationalNumber(91187654321L);
// 无效号码
private static final PhoneNumber US_LONG_NUMBER =
new PhoneNumber().setCountryCode(1).setNationalNumber(65025300001L);
}
2. 测试数据分类表
| 类别 | 示例号码 | 国家代码 | 号码 | 用途 |
|---|---|---|---|---|
| 固定电话 | US_NUMBER | 1 | 6502530000 | 格式验证 |
| 移动电话 | AR_MOBILE | 54 | 91187654321 | 类型识别 |
| 免费电话 | US_TOLLFREE | 1 | 8002530000 | 费用类型 |
| 国际号码 | INTERNATIONAL_TOLL_FREE | 800 | 12345678 | 跨国验证 |
| 无效号码 | US_LONG_NUMBER | 1 | 65025300001L | 错误处理 |
3. 动态测试数据生成
对于需要大量测试数据的场景,可以使用工厂方法:
public class PhoneNumberTestFactory {
public static PhoneNumber createUSNumber(long nationalNumber) {
return new PhoneNumber()
.setCountryCode(1)
.setNationalNumber(nationalNumber);
}
public static PhoneNumber createInternationalNumber(int countryCode, long nationalNumber) {
return new PhoneNumber()
.setCountryCode(countryCode)
.setNationalNumber(nationalNumber);
}
public static List<PhoneNumber> generateTestNumbers(String region, int count) {
List<PhoneNumber> numbers = new ArrayList<>();
int countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(region);
for (int i = 0; i < count; i++) {
long nationalNumber = 1000000000L + i;
numbers.add(new PhoneNumber()
.setCountryCode(countryCode)
.setNationalNumber(nationalNumber));
}
return numbers;
}
}
单元测试编写模式
1. 基础功能测试
public void testFormatUSNumber() {
assertEquals("650 253 0000",
phoneUtil.format(US_NUMBER, PhoneNumberFormat.NATIONAL));
assertEquals("+1 650 253 0000",
phoneUtil.format(US_NUMBER, PhoneNumberFormat.INTERNATIONAL));
}
public void testIsNumberGeographical() {
assertFalse(phoneUtil.isNumberGeographical(BS_MOBILE)); // 巴哈马移动号码
assertTrue(phoneUtil.isNumberGeographical(AU_NUMBER)); // 澳大利亚固定号码
assertFalse(phoneUtil.isNumberGeographical(INTERNATIONAL_TOLL_FREE)); // 国际免费电话
}
2. 边界条件测试
public void testGetNationalSignificantNumber_ManyLeadingZeros() {
PhoneNumber number = new PhoneNumber();
number.setCountryCode(1);
number.setNationalNumber(650);
number.setItalianLeadingZero(true);
number.setNumberOfLeadingZeros(2);
assertEquals("00650", phoneUtil.getNationalSignificantNumber(number));
// 测试异常值处理
number.setNumberOfLeadingZeros(-3);
assertEquals("650", phoneUtil.getNationalSignificantNumber(number));
}
3. 异常场景测试
public void testParseInvalidNumber() {
try {
phoneUtil.parse("invalid-number", "US");
fail("Expected NumberParseException");
} catch (NumberParseException e) {
assertEquals(NumberParseException.ErrorType.NOT_A_NUMBER, e.getErrorType());
}
}
测试数据管理策略
1. 国家代码映射测试数据
public class CountryCodeToRegionCodeMapForTesting {
public static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {
Map<Integer, List<String>> map = new HashMap<>();
map.put(1, Arrays.asList("US", "BS", "BB"));
map.put(44, Arrays.asList("GB", "GG"));
map.put(49, Arrays.asList("DE"));
map.put(54, Arrays.asList("AR"));
map.put(61, Arrays.asList("AU", "CX"));
map.put(64, Arrays.asList("NZ"));
map.put(65, Arrays.asList("SG"));
// ... 更多映射关系
return map;
}
}
2. 元数据测试文件结构
test/
└── com/
└── google/
└── i18n/
└── phonenumbers/
└── data/
├── PhoneNumberMetadataProtoForTesting_US
├── PhoneNumberMetadataProtoForTesting_DE
├── PhoneNumberMetadataProtoForTesting_GB
├── PhoneNumberMetadataProtoForTesting_AR
├── PhoneNumberMetadataProtoForTesting_AU
└── PhoneNumberMetadataProtoForTesting_800
高级测试技巧
1. 参数化测试
public class PhoneNumberFormatTest {
@ParameterizedTest
@MethodSource("formatTestData")
void testNumberFormatting(PhoneNumber number, PhoneNumberFormat format, String expected) {
assertEquals(expected, phoneUtil.format(number, format));
}
private static Stream<Arguments> formatTestData() {
return Stream.of(
Arguments.of(US_NUMBER, PhoneNumberFormat.NATIONAL, "650 253 0000"),
Arguments.of(US_NUMBER, PhoneNumberFormat.INTERNATIONAL, "+1 650 253 0000"),
Arguments.of(GB_NUMBER, PhoneNumberFormat.NATIONAL, "(020) 7031 3000"),
Arguments.of(DE_NUMBER, PhoneNumberFormat.NATIONAL, "030/1234")
);
}
}
2. 性能测试
public class PhoneNumberPerformanceTest {
@Test
void testParsePerformance() {
int iterations = 10000;
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
phoneUtil.parse("+16502530000", "US");
}
long duration = System.nanoTime() - startTime;
double averageTime = (double) duration / iterations;
assertTrue(averageTime < 100000,
"Average parse time should be less than 100 microseconds");
}
}
测试覆盖率分析
通过系统化的测试策略,libphonenumber实现了高测试覆盖率:
最佳实践总结
1. 测试数据管理
- 使用静态常量定义核心测试用例
- 按国家/类型分类组织测试数据
- 覆盖边界情况和异常场景
2. Mock对象使用
- 隔离外部依赖如元数据加载器
- 模拟异常场景测试错误处理
- 验证调用次数确保性能优化
3. 测试策略
- 分层测试从单元到集成
- 参数化测试提高覆盖率
- 性能基准确保响应速度
4. 维护性考虑
- 清晰的测试命名便于理解
- 独立的测试数据避免耦合
- 完整的断言信息便于调试
结语
libphonenumber的单元测试体系为我们展示了如何为复杂国际电话号码处理库构建健壮的测试框架。通过合理的Mock对象使用、系统的测试数据生成策略以及全面的测试场景覆盖,确保了库在各种边界条件下的正确性和稳定性。
掌握这些测试编写技巧,不仅能够提升libphonenumber的使用效果,更能为其他国际化项目的测试实践提供宝贵参考。记住:好的测试是代码质量的基石,特别是在处理像电话号码这样具有复杂国际规则的领域时。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



