对 Android 应用来说,货币处理并不只是把金额除以 100 再拼接符号。一个典型的订单模块会涉及 java.util.Currency、java.text.NumberFormat、区域信息 Locale 以及设备底层的 ICU 数据。测试这类逻辑时,最棘手的是同一段格式化代码在本地 JVM、Robolectric 模拟环境与真机上可能得到不同结果。因此,把货币测试单独作为一个主题来设计,能避免后续回归带来的隐蔽问题。

一、先厘清 Currency 与 NumberFormat 的测试边界
业务代码里经常出现一种误解:把 Currency 当成汇率转换工具。实际上 Currency 只表示 ISO 4217 货币代码,例如 USD、CNY、EUR,它不持有汇率,也不负责金额换算。它的职责是提供货币的小数位、符号、国家或地区映射等静态信息。如果你在测试中期望 Currency 自动完成美元转人民币,那这条用例从一开始就不会通过。
以基础格式化测试为例,需要同时验证币种与区域两个变量。下面的用例验证美元在 Locale.US 下是否输出美元符号和千位分隔符。这里有一个关键点:NumberFormat.getCurrencyInstance 返回的是与默认区域绑定的格式化器,必须显式调用 setCurrency 才能把目标币种应用到格式化器,否则可能出现区域与货币不匹配的情况。
import org.junit.Test;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class BasicCurrencyFormatTest {
@Test
public void usdFormat_shouldContainDollarSignAndGrouping() {
Currency usd = Currency.getInstance("USD");
NumberFormat fmt = NumberFormat.getCurrencyInstance(Locale.US);
fmt.setCurrency(usd);
String result = fmt.format(1234.56);
assertEquals("$1,234.56", result);
}
}
这个测试稳定,是因为它只依赖 JDK 自带的货币数据,不涉及 Android 运行时资源。但在真实 Android 环境中,Currency.getSymbol 与 NumberFormat 会读取 ICU 数据,不同 API 级别可能有细微差异。例如部分旧设备对 CNY 的本地化符号显示为 CN¥,而新设备可能只显示 ¥。如果只依赖纯 JVM 测试,这些平台差异会被掩盖,上线后反而更难排查。
二、基于 JUnit 与 Robolectric 搭建稳定的货币测试环境
要覆盖 Android 运行时行为,又不希望每次测试都启动模拟器,Robolectric 是比较合适的中间方案。它可以在 JVM 上模拟 Android SDK,让 NumberFormat 和 Currency 的调用路径更接近真机。配置时需要用 @RunWith(RobolectricTestRunner.class) 替换默认的 JUnit4 运行器,并通过 @Config(sdk = 28) 固定 SDK 版本,避免不同开发机器上的默认版本导致测试结果漂移。
固定 SDK 只是第一步,更关键的是统一 Locale。不同测试机器的系统区域可能不同,例如一台是 Locale.US,另一台是 Locale.GERMANY,同样的金额 1234.56 会被格式化成 $1,234.56 或 1.234,56 €。因此不要把默认区域带进货币测试,应该在测试方法内显式指定 Locale,必要时为测试基类设置固定的默认区域。
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28)
public class RobolectricCurrencyTest {
private NumberFormat usdFormat;
@Before
public void setUp() {
usdFormat = NumberFormat.getCurrencyInstance(Locale.US);
usdFormat.setCurrency(Currency.getInstance("USD"));
}
@Test
public void usdFormat_shouldReturnUsdPattern() {
assertEquals("$1,234.56", usdFormat.format(1234.56));
}
@Test
public void eurFormat_shouldUseGermanPatternWhenLocaleIsGermany() {
NumberFormat fmt = NumberFormat.getCurrencyInstance(Locale.GERMANY);
fmt.setCurrency(Currency.getInstance("EUR"));
String result = fmt.format(1234.56);
assertTrue(result.contains("€"));
assertTrue(result.startsWith("1.234"));
}
}
这段代码通过 @Before 初始化格式化器,并在每个测试方法中显式指定 Locale。当测试运行失败时,先检查是否缺少 Robolectric 依赖,再确认 unitTests.includeAndroidResources 是否开启。不少货币测试失败并不是断言本身有问题,而是环境没有真正加载 Android 资源,导致格式化路径退回到纯 JVM 实现。
如果项目还依赖 android.icu.text.NumberFormat,需要特别注意它与 java.text.NumberFormat 在缓存和符号回退上的区别。Robolectric 默认对 ICU 的支持有限,必要时可以在测试配置中指定真实设备资源,或者把这部分逻辑延迟到仪器测试。
三、多币种场景与格式化边界的测试策略
实际业务里经常会同时展示多种货币,例如电商 App 的商品列表需要根据用户切换显示 USD、EUR、JPY。JPY 没有小数位,Currency.getDefaultFractionDigits() 返回 0,而大多数币种返回 2。这个差异容易被忽略,导致价格展示出现 ¥1,235 与 ¥1,235.00 不一致。通过参数化测试可以批量验证不同币种的小数位配置。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class CurrencyFractionDigitsTest {
private final String currencyCode;
private final int expectedFractionDigits;
public CurrencyFractionDigitsTest(String currencyCode, int expectedFractionDigits) {
this.currencyCode = currencyCode;
this.expectedFractionDigits = expectedFractionDigits;
}
@Parameterized.Parameters
public static List<Object[]> data() {
return Arrays.asList(new Object[][]{
{"USD", 2},
{"CNY", 2},
{"JPY", 0},
{"BHD", 3},
{"KWD", 3}
});
}
@Test
public void currency_shouldReportCorrectFractionDigits() {
Currency currency = Currency.getInstance(currencyCode);
assertEquals(expectedFractionDigits, currency.getDefaultFractionDigits());
}
}
这个参数化用例直接验证 Currency 的元数据,不依赖区域和格式化器,因此很适合放在纯 JVM 测试层。类似地,还可以对 Currency.getNumericCode、Currency.getSymbol(Locale) 建立对照表。不过符号验证最好放在 Robolectric 或仪器测试中,因为部分符号来自 ICU 资源,JDK 与 Android 的取值路径不同。
更复杂的场景包括货币符号位置、负金额格式、阿拉伯地区使用阿拉伯数字、以及 setMaximumFractionDigits 覆盖币种默认小数位。建议将货币格式化规则集中在一个工具类中,再把工具类作为参数化测试的输入,避免在 Activity 或 ViewModel 里散落格式逻辑。这样即便以后要支持多语言或切换记账币种,测试用例也不用大面积改动。
如果项目需要更高保真的设备差异覆盖,应补充仪器测试,在 API 21、26、28、33 等代表性版本上各跑一遍。Robolectric 快速反馈适合日常提交,但不能完全代替真机。尤其涉及系统输入法、无障碍服务读取货币文本时,真机行为不可替代。把单元测试、Robolectric 测试和仪器测试分层,能让货币逻辑在多次迭代中保持稳定。
Android货币测试Currency单元测试JUnit修改时间:2026-09-19 07:50:48