Test-Driven Development in Android MVVM Architecture (Part 2-Setup & Unit testing for LiveData)
So here we go…!!!
Before begin, if you didn’t check with my previous article, I recommend you check it here.
We have a small sample android application that hosts a Login screen and Home screen. You can clone it here. I recommend you to clone this repo and checkout to tdd-starter-app
branch, so you can easily understand the flow ✌.
git clone https://github.com/shehzab.developer/Test_Driven_Development.git
git checkout -b tdd-starter-app
So, now you had cloned this repo and checkout to tdd-starter-app.
Let's start to swim (I mean to code😊 )
We will start step by step. Lets first start with a sample test method.
The test you write will check that when you call the validateUser
, the Boolean
value will get fired. Go and check your LoginViewModel
class for this method.
So let's start!!!
Step 1:Open your project, goto package login
, open LoginViewModel
class.
Step 2: Right-click LoginViewModel
and select Generate > test
LoginViewModel and select Generate
Step 3: Pop-up appears. Select default values (cross-check with the image below) and click Ok.
Step 4: Pop-up appears. Select a test and click Ok.
Notice the generated LoginViewModelTest
class in test/login/
package.
Step 5: Now in your LoginViewModelTest
class write a new test called validateUser_Boolean
.
LoginViewModelTest.kt
class LoginViewModelTest {
@Test
fun validateUser_Boolean() {
}
}
But wait for the second... What about the application context???
So here it comes AndroidX Test libraries which include classes and methods that provide you with versions of components like Applications and Activities that are meant for tests. So, just follow the below steps to set up AndroidX Test. Add below dependencies into your app module’s build.gradle.
1. Add the AndroidX core and ext dependencies.testImplementation 'androidx.test.ext:junit-ktx:1.1.1'
testImplementation 'androidx.test:core-ktx:1.2.0'
2. Add the Robolectric testing library dependencies.testImplementation 'org.robolectric:robolectric:4.3.1'
3. Annotate the class with the AndroidJUnit4 test runner.@RunWith(AndroidJUnit4::class)
4. Write the AndroidX test code.val loginViewModel= LoginViewModel(ApplicationProvider.getApplicationContext())
So now we got an application context.
Now call validateUser
on loginViewModel
.val user = User("android","123456")
loginViewModel.validateUser(user)
So code look like:
@RunWith(AndroidJUnit4::class)
class LoginViewModelTest {
@Test
fun validateUser_Boolean() {
val loginViewModel = LoginViewModel(ApplicationProvider.getApplicationContext())
val user = User("android","123456")
loginViewModel.validateUser(user)
}
}
We can take out application context call to @Before
method and make an assert call to check the test.
@RunWith(AndroidJUnit4::class)
class LoginViewModelTest {
private lateinit var loginViewModel: LoginViewModel
@Before
fun setup() {
loginViewModel = LoginViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun validateUser_Boolean() {
val user = User("android", "123456")
assertTrue(loginViewModel.validateUser(user))
}
}
Run the test. Cool... the test passed.
So it’s just started. Our aim is to test LiveData
value. So just follow the same things and call doLogin
from LoginViewModel
where the return type is LiveData
value.
Step 6: Now in your LoginViewModelTest
class write a new test called doLogin_LiveData
.
@Test
fun doLogin_LiveData() {
val user = User("android", "123456")
loginViewModel.doLogin(user)
//TODO test LiveData
}
To test LiveData
, we need two things.
1. Use InstantTaskExecutorRule
.testImplementation 'androidx.arch.core:core-testing:2.1.0'
2. Ensure LiveData
observation.
InstantTaskExecutorRule
is a JUnit Rule. We can use it with @get:Rule
annotation, it causes some code in the InstantTaskExecutorRule
class to be run before and after the test.
When we use LiveData
, we commonly have activity or fragment to observe the LiveData
. This makes a problem in our LoginViewModelTest
class because we don't have any activity or fragments to observe it. To get around this, we need to use observeForever
method, which ensures the LiveData
is constantly observers, without needing a LifecycleOwner
.
To get over this, create a Kotlin file name as LiveDataTestUtil.kt
and copy-paste the below code to this file.
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@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
}
So, the above code will take care of LiveData
observer and we don't need to put our hands on it.
So now our code looks like
.... @get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
....
....
....
@Test
fun doLogin_LiveData() {
val user = User("android", "123456")
loginViewModel.doLogin(user)
assertTrue(loginViewModel.response.getOrAwaitValue())
}
}
Cool… the test passed!!!
For complete source code for this chapter, checkout repo to tdd-unit-testing.
git checkout -b tdd-unit-testing
What’s Next?
Next, we are going to dive into the UI stuff. Check it out Part-3 by clicking here or in the below link.
Thank you for your time to read this article. If you like it please give me your clap 👏 , so others can find it too.