🏌A Simplified Android Ktx ViewModel Test ⛳️
You cannot test Ktx ViewModel directly if you switch from val to var. The system will give you a ❌ RED flag at once. Unless we wrap it up with a lazy function. Here is my process with my example:
— === Menu === —
🔌1. Gradle.Module
🔑2. Add ProvideModel Function
⚜………………………..⭐ProvideViewModel.kt
🔬 3. Test the ViewModel
⚜………………………..🏭 Fragment Factory
⚜………………………..✔️ LiveData Support Function
⚜………………………..✅ Test the ViewModel
🔌1. Gradle.Module …… → Menu
Add this line to your Gradle.module:
// Kotlin Reflect
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
🔑2. Add ProvideModel Function …… → Menu
With Google ViewModel Ktx version, for example,
val viewModel: ShoppingViewModel by viewModels()
You can not test this viewModel in the test case. Unless you go back to the old way:
lateinit var viewModel: ShoppingViewModelonCreate() {
viewModel = ViewModelProvider(...)
}
Let’s add a function to make Ktx be testable by adding ⭐ ProvideViewModel.kt: …… → Menu
inline fun <reified VM : ViewModel> Fragment.provideViewModel(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> =
OverridableLazy(viewModels(ownerProducer, factoryProducer))
// wrapper
class OverridableLazy<T>(var implementation: Lazy<T>): Lazy<T> {
override val value
get() = implementation.value
override fun isInitialized() = implementation.isInitialized()
}
// fragment extended function
fun <VM: ViewModel, T> T.replace(
viewModelDelegate: KProperty1<T, VM>, viewModel: VM) {
viewModelDelegate.isAccessible = true
(viewModelDelegate.getDelegate(this) as
OverridableLazy<VM>).implementation = lazy { viewModel }
}
Replace the viewModels() in the Fragment:
val viewModel: ShoppingViewModel by provideViewModel()
🔬 3. Test the ViewModel …… → Menu
🏭 Fragment Factory …… → Menu
Let’s add a Fragment Factory to instantiate the Fragment.
⭐ CoolFragmentFactory.kt
class CoolFragmentFactory @Inject constructor(): FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
// dispatch by className
return when (className) {
YourFragment::class.java.name -> YourFragment()
else -> super.instantiate(classLoader, className)
}
}
}
Inject the factory into the YourFragmentTest.kt.
@MediumTest
@HiltAndroidTest
@ExperimentalCoroutinesApi
class YourFragmentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Inject
lateinit var fragmentFactory: CoolFragmentFactory
@Before
fun setup() {
hiltRule.inject()
}
...
✔️ LiveData Support Function …… → Menu
Copied from Google,
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
✅ Test the ViewModel …… → Menu
For example,
@Test
fun backpressed_setImageUrl_blank_test() {
val imageUrl = "Test"
val testViewModel = YourViewModel(...)
testViewModel.setCurImageUrl(imageUrl)
launchFragmentInHiltContainer<YourFragment>(
fragmentFactory = fragmentFactory
) {
navController.setGraph(R.navigation.cool_nav)
Navigation.setViewNavController(
requireView(),
navController)
navController.navigate(R.id.add_item)
this.replace(
YourFragment::viewModel,
testViewModel)
}
Espresso.pressBack()
runOnUiThread {
Truth.assertThat(testViewModel.curImgUrl.getOrAwaitValue())
.isEqualTo("")
}
}
In this example, you’ll found that is very simple to replace the Ktx viewModels() with testViewModel. And I can test its LiveData, too. The LiveData has a preset URL as “Test”. When I press the back button, the LiveData shall be reset to blank.
The test has passed! Isn’t that great?