Hi Guys, welcome to Android ViewModel Unit Test Tutorial. This post is also part of our Android Testing Series. In this post, we will learn how to test our ViewModels.
So far, we have learned about writing Unit Tests using JUnit4 and Instrumented Unit Test using AndroidJUnit4. We also learned writing tests for Room Database.
Now, let’s begin testing ViewModel.
Table of Contents
Android ViewModel Unit Test Tutorial
I will be working on the same project. And for first-timers, we are creating an application that is called Spend Tracker. I have already made everything that is required. I will not be discussing the things like Creating ViewModel, DataSource, etc.
You can get my source code; the link is at the bottom of this post. I have the following ViewModel in my project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class SpendViewModel( private val dataSource: SpendsTrackerDataSource ) : ViewModel() { private val _last20SpendsLiveData = MutableLiveData<List>() val last20SpendsLiveData: LiveData<List> get() = _last20SpendsLiveData fun addSpend(amount: Int, description: String) = viewModelScope.launch { dataSource.addSpend(Spend(Date(), amount, description)) } fun getLast20Spends() = viewModelScope.launch { _last20SpendsLiveData.value = dataSource.getLast20Spends() } } |
As you can see in the above code, I have two functions:Â
- To insert a new spend into the database,
- To get the last 20 saved spend from the database.Â
Pretty much the same as we did while testing the room database, but this time we have these functions inside ViewModel, and we are operating in the database using a DataSource.Â
1 2 3 4 5 6 7 8 9 |
class SpendsTrackerDataSource( private val db: SpendDao ) { suspend fun addSpend(spend: Spend) = db.addSpend(spend) suspend fun getLast20Spends() = db.getLast20Spends() } |
As you can see, our DataSource requires the SpendDao, that we tested in the last post. To get SpendDao we need the Database Instance. Building the Database Instance requires Context, an Android-specific class; hence, we cannot do a normal Unit Test.
If you want to test your ViewModels using JUnit, you must try to keep your ViewModels independent from Android Specific Classes.
But, it’s not always possible to keep ViewModels independent like this, which is why we will learn about Roboelectric as well. That will help us to run this kind of test without using an emulator.
Testing ViewModel
Now, let’s test our ViewModel. To do this again, we will generate a test class inside the androidTest package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@RunWith(AndroidJUnit4::class) class SpendViewModelTest : TestCase() { private lateinit var spendsDatabase: SpendsDatabase private lateinit var viewModel: SpendViewModel @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Before public override fun setUp() { val context = ApplicationProvider.getApplicationContext<Context>() spendsDatabase = Room.inMemoryDatabaseBuilder( context, SpendsDatabase::class.java ).allowMainThreadQueries().build() val dataSource = SpendsTrackerDataSource(spendsDatabase.getSpendDao()) viewModel = SpendViewModel(dataSource) } @After @Throws(IOException::class) fun closeDb() { spendsDatabase.close() } @Test fun testAddingSpend() { viewModel.addSpend(100, "Eggs") viewModel.getLast20Spends() val result = viewModel.last20SpendsLiveData.getOrAwaitValue().find { it.amount == 100 && it.description == "Eggs" } assertThat(result != null).isTrue() } } |
The code above is pretty much same as last time except one new thing that is
@get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()Â
We have added this rule to swap the background executor. And this new executor will work synchronously.
Apart from this we are using the function getOrAwaitValue(), and we need to define this function. This function is to get the value from LiveData.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@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() if (!latch.await(time, timeUnit)) { throw TimeoutException("LiveData value was never set.") } } finally { this.removeObserver(observer) } @Suppress("UNCHECKED_CAST") return data as T } |
You can run your test but remember you need a physical device or emulator to run this test.
Using Roboelectric
As we are running an instrumented unit test here; and it requires a real device or emulator. And you know it is time taking to do things on a real device or emulator. So to make this process faster, we can use Roboelectric.
Roboelectric can simulate Android Environment in JVM only. And with it, you do not need an emulator or real device to run tests. And that is why it will make your tests very fast.
To use Roboelectric, first you need to add the following dependency.
1 2 3 |
testImplementation 'org.robolectric:robolectric:4.5.1' |
Then you can create the same test class inside test folder, with the following change.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
@Config(manifest = "src/main/AndroidManifest.xml") @RunWith(RobolectricTestRunner::class) class SpendViewModelTest1 : TestCase() { private lateinit var spendsDatabase: SpendsDatabase private lateinit var viewModel: SpendViewModel @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Before public override fun setUp() { val context = ApplicationProvider.getApplicationContext<Context>() spendsDatabase = Room.inMemoryDatabaseBuilder( context, SpendsDatabase::class.java ).allowMainThreadQueries().build() val dataSource = SpendsTrackerDataSource(spendsDatabase.getSpendDao()) viewModel = SpendViewModel(dataSource) } @After @Throws(IOException::class) fun closeDb() { spendsDatabase.close() } @Test fun testAddingSpend() { viewModel.addSpend(100, "Eggs") viewModel.getLast20Spends() val result = viewModel.last20SpendsLiveData.getOrAwaitValue().find { it.amount == 100 && it.description == "Eggs" } assertThat(result != null).isTrue() } } |
So this time we are using @RunWith(RobolectricTestRunner::class) annotation for the test. We have also defined @Config(manifest = "src/main/AndroidManifest.xml") but it is optional here.
Now you can simply run the test, but one more thing that you need to do is; you need to run this test using Java9.
As you can see I have defined the JRE as Java9. Now you can simply run the test and it will work without an emulator.
Android ViewModel Unit Test Source Code
You can get my project’s source code from here.
So that is all for this tutorial, friends. I hope you found this helpful and learned something. In case you have any problems or questions, leave your comments below. And don’t forget to share this Android ViewModel Unit Test Tutorial with your friends. Thank You 🙂