Test Driven Development with SingleLiveEvent
This is a continuation from a previous article.
Objectives
- Use SingleLiveEvent object to allow activities to observe for commands from the view model.
- Use sealed classes for setting the different type of events.
- Write tests to assert the type of commands set by view model.
SingleLiveEvent
SingleLiveEvent is a popular was for view models to “send” data or commands to an Android activity of fragment. Read about it more in this article.
Here are some characteristics of the SingleLiveEvent class
- It is an implementation of LiveData (extends from MutableLiveData)
- It should only have one observer
- It only signals the observer when a value is being set.
A SingleLiveEvent is commonly used to instruct an activity to perform an action. It is also often to find them being used with sealed classes.
Requirements
- View model gets data from a repository.
- If there is no error, we update a live data.
- If there is an error, we instruct the activity to inform the user.
We will be using the SingleLiveEvent to signal to the activity that an error has occurred. As for what form (Toast, Snackbar, DialogFragment), we view model simply do not care.
So here is a simple diagram of how things should happen if DataRepository returns an error.
Setup
Download the project covered in our first article at https://github.com/SPHTech/TestDrivenMVVM. It will be easier as some test rules are already there.
Copy and create a SingleLiveEvent class in the project. You can copy the code from here.
Create a sealed class called ViewCommand.kt. We can think of sealed classes as enums.
sealed class ViewCommand {
class DataError(val message: String) : ViewCommand()
}
Create a SingleLiveEvent of type ViewCommand in the view model.
val viewCommand = SingleLiveEvent<ViewCommand>()
All set, now it is time for test cases.
Test Cases
By the way, DataRepository has this interface.
interface DataRepository {
fun fetchData(): Observable<String>
}
- Configure the mock DataRepository to return an error.
whenever(mockDataRepository.fetchData())
.thenReturn(Observable.error(Throwable("error")))
- Create a mock observer.
@Mock
lateinit var mockViewCommandObserver: Observer<ViewCommand>
- Register the mock observer with our new live data.
myViewModel.viewCommand.observeForever(mockViewCommandObserver)
- Fire the method under test
getStuff()
.
myViewModel.getStuff()
- Assert mock observer’s onChange called.
verify(mockViewCommandObserver, times(1)).onChanged(any())
Improving Your Test Cases
We have now test cases to check that the viewCommand live data is set upon when there is an error from DataRepository. We should further check that it is of the type DataError()
class.
We will use ArgumentMatchers from Mockito.
ArgumentMatchers.any(ViewCommand.DataError::class.java)
And change the verification portion of our test to,
verify(mockViewCommandObserver, times(1))
.onChanged(
ArgumentMatchers.any(ViewCommand.DataError::class.java))
So now this test fails if viewCommand live data is being set with any other type of class.
Test to Check Error Message
We can write another test to check that the error message is one that you expected. The first few steps is similar. Just change the verify(...)
method to and Assert.assertEquals(...)
.
@Test
fun testErrorMessageIsCorrect() { whenever(mockDataRepository.fetchData())
.thenReturn(Observable.error(Throwable("error")))
myViewModel.viewCommand.observeForever(mockViewCommandObserver)
myViewModel.getStuff()
Assert.assertEquals(
"error",
(myViewModel.viewCommand.value as ViewCommand.DataError)
.message)
}
Here you are trying to make sure the error message is “error”.
Conclusions
We used SingleLiveEvent to signal to Activities or Fragments to perform something. We create test cases to make sure a SingleLiveEvent data has its value set when an error from DataRepository occurred. We also created test to check the correct type and message. And we did all these without writing any implementation in our getStuff()
function.
To convince yourself, write some implementation code to pass these failing test cases. One such implementation could be this,
fun getStuff(){
dataRepository.fetchData().subscribe({
// onNext case
}, {
viewCommand.value =
ViewCommand.DataError(it.message?:"backup message")
})
}