MVVM架构下使用String Resources的最佳实践?Kotlin单元测试遇Context难题
问题场景
我在编写单元测试时,遇到工具类(比如示例中的testClass)依赖Context来获取字符串资源的问题。原本想通过这类工具类拆分ViewModel逻辑、实现复用,但测试时必须模拟Context,操作不够简便。
现有代码实现
工具类testClass
class testClass { fun compareNumbers(numberOne: Int, numberTwo: Int, context: Context): String { var message = "" when { numberOne > numberTwo -> message = context.getString(R.string.numberOne) numberOne < numberTwo -> message = context.getString(R.string.numberTwo) numberOne == numberTwo -> message = context.getString(R.string.tie) } return message } }
ViewModel中的调用
fun declareWinner(context: Context) { val result = testClass.compareNumbers(numberOne, numberTwo, context) updateResult(result) //updates the result to a database }
优化方案
1. 分离业务逻辑与资源获取
把数值比较的业务逻辑和字符串资源获取拆分开,工具类只负责返回逻辑结果(比如枚举),由调用方(ViewModel)处理资源获取。这样工具类完全不依赖Context,测试零成本。
示例改造:
// 定义枚举表示比较结果 enum class CompareResult { NUMBER_ONE_GREATER, NUMBER_TWO_GREATER, TIE } class NumberComparator { fun compareNumbers(numberOne: Int, numberTwo: Int): CompareResult { return when { numberOne > numberTwo -> CompareResult.NUMBER_ONE_GREATER numberOne < numberTwo -> CompareResult.NUMBER_TWO_GREATER else -> CompareResult.TIE } } }
ViewModel中处理资源:
fun declareWinner(context: Context) { val result = numberComparator.compareNumbers(numberOne, numberTwo) val message = when(result) { CompareResult.NUMBER_ONE_GREATER -> context.getString(R.string.numberOne) CompareResult.NUMBER_TWO_GREATER -> context.getString(R.string.numberTwo) CompareResult.TIE -> context.getString(R.string.tie) } updateResult(message) }
这种方式的好处是:工具类的单元测试只需验证数值与枚举的对应关系,完全无需处理Context;ViewModel的测试也可专注于枚举到字符串的映射逻辑,或用Mockito轻量模拟Context。
2. 注入字符串资源提供者(依赖倒置)
如果不想把资源逻辑放到ViewModel里,可以定义资源提供接口,让工具类依赖接口而非具体Context,测试时用Mock实现接口即可。
示例:
// 定义资源提供接口 interface StringResourceProvider { fun getString(resId: Int): String } // Android环境下的资源提供者实现 class AndroidStringResourceProvider(private val context: Context) : StringResourceProvider { override fun getString(resId: Int): String { return context.getString(resId) } } // 工具类依赖接口 class NumberComparator(private val resourceProvider: StringResourceProvider) { fun compareNumbers(numberOne: Int, numberTwo: Int): String { return when { numberOne > numberTwo -> resourceProvider.getString(R.string.numberOne) numberOne < numberTwo -> resourceProvider.getString(R.string.numberTwo) else -> resourceProvider.getString(R.string.tie) } } }
ViewModel中注入实现类:
// ViewModel初始化时传入AndroidStringResourceProvider val numberComparator = NumberComparator(AndroidStringResourceProvider(context)) fun declareWinner() { val result = numberComparator.compareNumbers(numberOne, numberTwo) updateResult(result) }
测试时Mock接口:
// 单元测试示例 @Test fun testCompareNumbers() { val mockProvider = mock(StringResourceProvider::class.java) `when`(mockProvider.getString(R.string.numberOne)).thenReturn("Number One wins") val comparator = NumberComparator(mockProvider) val result = comparator.compareNumbers(5, 3) assertEquals("Number One wins", result) }
这种方式既保留了工具类的功能完整性,又降低了与Android框架的耦合,测试灵活性大幅提升。
3. 用AndroidX Test的InstrumentationRegistry(仅Instrumented测试)
如果测试必须验证真实资源(需跑在真机/模拟器上),可以用AndroidX Test提供的InstrumentationRegistry获取真实Context,无需Mockito:
import androidx.test.platform.app.InstrumentationRegistry @Test fun testCompareNumbersWithRealContext() { val context = InstrumentationRegistry.getInstrumentation().targetContext val comparator = testClass() val result = comparator.compareNumbers(5, 3) assertEquals(context.getString(R.string.numberOne), result) }
注意:这是Instrumented测试,运行速度远慢于纯单元测试,仅适合必须验证真实资源的场景。
总结
优先选方案1,它彻底分离业务逻辑与Android依赖,单元测试最简单;若需保持工具类的资源获取能力,选方案2,通过依赖倒置降低耦合;方案3仅适合验证真实资源的场景。
内容的提问来源于stack exchange,提问作者Hunter K

