🏌A Simplified Android Ktx ViewModel Test ⛳️

Homan Huang
3 min readApr 21, 2021

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?

🥳Try it yourself! Have fun! …… → Menu

--

--

Homan Huang

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.