13.
Testing MVVM
Written by Aldo Olivares
In the previous chapters, you learned how to implement the MVVM architecture by using different technologies such as the ViewModel and LiveData from Google’s Architecture Components. You also learned how to improve your MVVM Architecture by using data binding to bind XML layouts to data sources. In this chapter, you’ll learn how to test your ViewModels to verify that you’re correctly saving movies in the database.
Getting started
Start by opening the starter project for this chapter. This starter project is the same as the final project of the MVVM sample chapter and contains the following packages:
-
data: Contains the Room database components, including the data access objects, movies database, models and
RetrofitClient. - viewmodel: Contains the ViewModels.
- view: Contains the Activities and Adapters.
Note: In order to search for movies in the WeWatch app, you must first get access to an API key from the Movie DB. To get your API own key, sign up for an account at www.themoviedb.org. Then, navigate to your account settings on the website, view your settings for the API, and register for a developer API key. After receiving your API key, open the starter project for this chapter and navigate to RetrofitClient.kt. There, you can replace the existing value for
API_KEYwith your own.
Build and run the app to verify everything is working.
Creating unit tests
WeWatch has three ViewModels:
-
MainViewModel: UsesMovieRepositoryto retrieve a list of saved movies that are stored in the Room database. -
AddViewModel: UsesMovieRepositoryto store a new movie in the database if it meets the appropriate criteria. -
SearchViewModel: UsesMovieRepositoryto retrieve a list of movies that match the given query parameter from the TMDB API.
Because the only functionality of MainViewModel and SearchViewModel is to retrieve movies from a different data source, you’ll focus on AddViewModel to create unit tests.
Inside viewmodel, open AddViewModel.kt and select AddViewModel. Then, click Alt-Enter and select Create test:
On the next screen, use the default values and click OK:
You may get another window pops up which looks as follows:
Select the option that includes test in the folder path and click OK; do not select androidTest. This should immediately create a new AddViewModelTest class inside the viewmodel package of the test directory.
Annotate the AddViewModelTest to use the MockitoJUnitRunner, and a AddViewModel property:
@RunWith(MockitoJUnitRunner::class)
class AddViewModelTest {
private lateinit var addViewModel: AddViewModel
}
Since you’ll be testing AddViewModel, you need to create an instance of it. You might remember that when writing unit tests, you need to assume that all of your dependencies are working. Since you’re focusing on a single component, you can use some tools to help — this is where mocking frameworks come in handy!
Mocking frameworks allow you to create a dummy implementation of an interface. With this dummy interface, you can easily configure it to produce the expected inputs an outputs.
So, instead of running the code in a dependency, you substitute that dependency with an object you can control: the mock.
Although it’s perfectly acceptable to create your own mocks, it’s more convenient to use a well-tested framework like Mockito. Mockito is a popular mocking framework for Java and Android that you can use along with JUnit to simplify the testing process of your classes.
Coming back to WeWatch: Because AddViewModel needs an instance of MovieRepository to save movies, you need to add the following property:
@Mock
lateinit var repository: MovieRepository
@Mock indicates that this is a mock object that will get initialized later.
Now, add the following method:
//1
@Before
fun setup() {
//2
addViewModel = AddViewModel(repository)
}
Here’s what’s happening:
- When creating tests, it’s common to create methods that initialize your objects and configure your mocks.
@Beforeindicates that this method needs to get executed before each test is run. - This line initializes
addViewModelusing the mock repository as a constructor parameter.
Now that viewmodel and the mocks are initialized, it’s time to write some tests!
The first thing you need to test is that there are no movies getting saved without a title, so add the following method:
//1
@Test
fun cantSaveMovieWithoutTitle() {
//2
addViewModel.title.set("")
addViewModel.releaseDate.set("")
//3
val canSaveMovie = addViewModel.canSaveMovie()
//4
assertEquals(false, canSaveMovie)
}
Here’s what’s happening:
-
@Testinforms JUnit that this method needs to get executed as a test. Each time a test runs, JUnit creates a fresh instance of the class and any exceptions thrown are reported as failures. - These lines set
titleandreleaseDateofaddViewModelto an empty string. - This stores the result of
saveMovie()tocanSaveMovie. -
assertEqualsverifies that the objects passed as parameters are equal. If they aren’t, an error is thrown and the test fails. BecausecanSaveMovie()returnsfalsewhentitleinAddViewModelis blank, this call should’t throw errors.
Click Run Test to the left of AddViewModelTest and select Run ‘AddViewModelTest’ to execute the tests:
Hurray! The test passes, which means canSaveMovie() is working as expected.
Next, you’ll add a feature that verifies whether or not a release date gets set for the movie. Normally, to add an extra feature to an app, you’d write the code for it and then verify that it’s working with a passing test. You could do it that way, but there’s something called Test Driven Development or TDD. Here’s how it works:
- Add a new test.
- Run the test and watch it fail. If it doesn’t fail, your job is done.
- If it fails, you write the necessary code that makes the test pass.
- Rinse and repeat until the test passes.
Following the TDD approach, you’ll implement this new feature by adding a test to verify that a movie can’t be saved without a release date and a title.
Add the following method to AddViewModelTest:
@Test
fun cantSaveMovieWithoutDate() {
addViewModel.title.set("Awesome Movie I")
addViewModel.releaseDate.set("")
val canSaveMovie = addViewModel.canSaveMovie()
assertEquals(false, canSaveMovie)
}
Here, you’re supplying a title for the movie with a blank release date. Because canSaveMovie() only verifies that title isn’t blank, it should return true, and the test should fail.
Now, execute all of the tests again by clicking Run Test, which is located to the left of AddViewModelTest. Selecting Run ‘AddViewModelTest’:
The test fails because now you’re expecting canSaveMovie() to return false when the release date is blank.
Excellent. The first and second steps of the TDD approach are complete: You have a failing test, and you watched it fail. It’s time for the third step: Writing the code to make the test pass.
Open AddViewModel.kt and modify canSaveMovie():
fun canSaveMovie(): Boolean {
val title = this.title.get()
val releaseDate = this.releaseDate.get()
if (title != null && releaseDate != null){
return title.isNotEmpty() && releaseDate.isNotEmpty()
}
return false
}
Although the basic logic is still the same, you’re now verifying that both title and releaseDate are populated by using isNotEmpty().
You also need to inform the users that both fields are mandatory. To accomplish this, you can modify the text displayed in the Snackbar. Inside resources/values, open strings.xml and add the following:
<string name="title_date_message">You must enter a title and release date</string>
Next, open AddMovieActivity.kt and modify configureLiveDataObservers() to use the new value:
private fun configureLiveDataObservers() {
viewModel.getSaveLiveData().observe(this, Observer { saved ->
saved?.let {
if (saved) {
finish()
} else {
showMessage(getString(R.string.title_date_message))//Only this line changes
}
}
})
}
That’s it! Go back to AddViewModelTest.kt and execute the test to watch it pass.
Great! You completed all of the necessary steps of TDD to implement a new feature. Build and run the app to see your new feature in action.
Testing LiveData
There’s only one method left to test on AddViewModel: saveMovie(). Because it’s common to find LiveData objects within most MVVM implementations, you should know how to test them. saveMovie() uses LiveData objects, so it’s perfect for this section.
Add the following method to AddViewModelTest:
@Test
fun isMovieProperlySaved(){
addViewModel.title.set("Awesome Movie II")
addViewModel.releaseDate.set("1994")
addViewModel.saveMovie()
assertEquals(true, addViewModel.getSaveLiveData().value)
}
This method sets a title and release date for a new movie on addViewModel. Because both fields have values, the new movie saves successfully in the database and getSaveLiveData() returns true, making the test pass.
Run the tests again to see what happens:
Oh no, the test fails!
Take a closer look at the logcat console. Notice the following messages:
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.os.Looper.getMainLooper(Looper.java)
at androidx.arch.core.executor.DefaultTaskExecutor.postToMainThread
at androidx.lifecycle.LiveData.postValue(LiveData.java:273)
As you might remember, LiveData is a DataHolder like a list or a hash map; it can contain any type of objects. You can change the value that your LiveData instance holds by using postValue() and setValue(). setvalue() immediately changes the value using the main thread, while postValue() asynchronously changes the value using a background thread.
When you create JUnit tests, they’ll always run on the main thread. But when you’re using postValue() in saveMovie() to update the LiveData object, that will run asynchronously.
Refactoring saveMovie() to use setValue() instead of postValue() might seem like an obvious and simple solution to the problem, but since database operations are long-running tasks, that will likely cause issues in your app. For the record, you never want to execute something that takes a long time on the main thread.
Recall back in the chapter about testing the MVP pattern, you created a custom TestRule to force all RxJava schedulers to run immediately when run inside of tests. Similarly, you’re going to add a new rule to your tests to indicate that all tasks should execute instantly, rather than asynchronously. Remember that TestRules only change the behavior of code during tests. In production, the code will run in parallel. This time, instead of creating your own custom TestRule, you’ll use one created by Google.
Open the app-level build.gradle and add the following line under the dependencies block:
testImplementation "androidx.arch.core:core-testing:2.0.0"
This dependency contains everything you need to perform unit testing for most of the Architecture Components such as LiveData.
Sync the project. Open AddViewModelTest.kt again and add the following:
@get:Rule
var rule: TestRule = InstantTaskExecutorRule()
@get:Rule indicates that this is a rule that all of your tests must be follow. In this case, the rule you’re using is InstantTaskExecutorRule(), which swaps the background executor used by the Architecture Components to a different one that executes your tasks synchronously.
Execute your tests one more time to see what happens.
Excellent! Everything passes.