Test ViewModels and LiveData

1. Before you begin

In previous codelabs you learned how to use ViewModels to handle business logic, as well as LiveData for reactive UIs. In this codelab, you'll learn how to write unit tests to check that your ViewModel code is working properly.

Prerequisites

  • You have created test directories in Android Studio.
  • You have written unit and instrumentation tests in Android Studio.
  • You have added Gradle dependencies to an Android project.

What you'll learn

  • How to write unit tests for ViewModels and LiveData.

What you need

  • A computer with Android Studio installed.
  • The solution code for the Cupcake app.

Download the starter code for this codelab

In this codelab you will add instrumentation tests to the Cupcake app from previous solution code.

To get the code for this codelab and open it in Android Studio, do the following.

Get the code

  1. Click on the provided URL. This opens the GitHub page for the project in a browser.
  2. Check and confirm the branch name matches with the branch name specified in the codelab. For example, in the following screenshot the branch name is main.

fe29aa9112862a93.png

  1. On the GitHub page for the project, click the Code button, which brings up a popup.

5b0a76c50478a73f.png

  1. In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
  2. Locate the file on your computer (likely in the Downloads folder).
  3. Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.

Open the project in Android Studio

  1. Start Android Studio.
  2. In the Welcome to Android Studio window, click Open.

a065e3d575fe607b.png

Note: If Android Studio is already open, instead, select the File > Open menu option.

4f3b1e628c7695f1.png

  1. In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
  2. Double-click on that project folder.
  3. Wait for Android Studio to open the project.
  4. Click the Run button 11c34fc5e516fb1c.png to build and run the app. Make sure it builds as expected.

2. Starter app overview

The Cupcake app consists of a home screen that shows an order screen with three options for cupcake quantities. Clicking an option takes you to a screen where you select a flavor, and then takes you to a screen to select a pick up date for the order. After that you can send your order to another app. You can cancel your order at any one of these stages.

3. Create the unit test directories

Create a unit test directory for the Cupcake app as you have done in previous codelabs.

4. Create a unit test class

Create a new class called ViewModelTests.kt.

5. Add the necessary dependencies

Add the following dependencies to your project:

testImplementation 'junit:junit:4.+'
testImplementation 'androidx.arch.core:core-testing:2.1.0'

Now sync your project.

6. Write a ViewModel test

Let's start with a simple test. The first thing we do when we interact with the app on a device or emulator is to select a quantity of cupcakes. So first we will test the setQuantity() method in the OrderViewModel, and check the value of the quantity LiveData object.

The quantity variable, which we are going to test, is an instance of LiveData. Testing LiveData objects requires an extra step, and this is where the dependency we added comes into play. We use LiveData to update our UI as soon as a value changes. Our UI runs on what we call the "main thread." If you are unfamiliar with threading and concurrency, that's okay, we'll go over it in depth in other codelabs. For the time being, in the context of an Android app, think of the main thread as the UI thread. The code that shows the UI to a user runs on this thread. Unless otherwise specified, a unit test assumes that everything runs on the main thread. However, because LiveData objects cannot access the main thread we have to explicitly state that LiveData objects should not call the main thread.

  1. To specify that LiveData objects should not call the main thread we need to provide a specific test rule any time we are testing a LiveData object.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
  1. Now we can create a function called quantity_twelve_cupcakes(). In the method, create an instance of the OrderViewModel.
  2. In this test, you will be checking to make sure the quantity object in the OrderViewModel is updated when setQuantity is called. But before calling any methods or working with any data in the OrderViewModel, it is important to note that when testing the values of a LiveData object, the objects need to be observed in order for changes to be emitted. A simple way of doing this is by using the observeForever method. Call the observeForever method on the quantity object. This method requires a lambda expression, but that can be left empty.
  3. Then call the setQuantity() method, passing in 12 as a parameter.
val viewModel = OrderViewModel()
viewModel.quantity.observeForever {}
viewModel.setQuantity(12)
  1. We can safely infer that the value of the quantity object is 12. Note that LiveData objects are not the value itself. Values are contained in a property called value. Make the following assertion:
assertEquals(12, viewModel.quantity.value)

Your test should look like this:

@Test
fun quantity_twelve_cupcakes() {
   val viewModel = OrderViewModel()
   viewModel.quantity.observeForever {}
   viewModel.setQuantity(12)
   assertEquals(12, viewModel.quantity.value)
}

Run your test! Congratulations, you just wrote your first LiveData unit test, which is a critical skill in modern Android development. This test doesn't test much business logic, so let's write a slightly more involved test.

One of the main functions of the OrderViewModel is to calculate the price of our order. This happens when we select a quantity of cupcakes, and when we select a pick up date. The price calculation happens in a private method, so our test cannot call this method directly. Only other methods in the OrderViewModel can call it. Those methods are public, so we'll call those in order to trigger the price calculation so we can check that the value of the price is what we expect.

Best practices

The price is updated when the quantity of cupcakes is selected, and when the date is selected. Although both of these should be tested, it's generally preferable to test only for a single functionality. Therefore, we'll make separate methods for each test: one function to test the price when the quantity is updated, and a separate function to test the price when the date is updated. We never want the outcome of a test to fail because a different test failed.

  1. Create a method called price_twelve_cupcakes() and annotate it as a test.
  2. In the method, create an instance of the OrderViewModel and call the setQuantity() method, passing in 12 as a parameter.
val viewModel = OrderViewModel()
viewModel.setQuantity(12)
  1. Looking at the PRICE_PER_CUPCAKE in OrderViewModel, we can see that cupcakes are $2.00 each. We can also see that resetOrder() is called every time the ViewModel is initialized, and in this method, the default date is today's date, and PRICE_FOR_SAME_DAY_PICKUP is $3.00. Therefore, 12 * 2 + 3 = 27. We expect that the value of the price variable, after selecting 12 cupcakes, to be $27.00. So let's make an assertion that our expected value of $27.00 equals the value of the price LiveData object.
assertEquals("$27.00", viewModel.price.value)

Now run the test.

It should fail!

17c8a24e4d7d635d.png

The test result says that our actual value was null. There is an explanation for this. If you look at the price variable in OrderViewModel you will see this:

val price: LiveData<String> = Transformations.map(_price) {
   // Format the price into the local currency and return this as LiveData<String>
   NumberFormat.getCurrencyInstance().format(it)
}

This is an example of why LiveData should be observed in testing. The value of price is set by using a Transformation. Essentially, this code takes the value that we assign to price and transforms it to a currency format so we don't have to do it manually. However, this code has other implications. When transforming a LiveData object, the code doesn't get called unless it absolutely has to be, this saves resources on a mobile device. The code will only be called if we observe the object for changes. Of course, this is done in our app, but we also need to do the same for the test.

  1. In your test method, add the following line before setting the quantity:
viewModel.price.observeForever {}

Your test should look like this:

@Test
fun price_twelve_cupcakes() {
   val viewModel = OrderViewModel()
   viewModel.price.observeForever {}
   viewModel.setQuantity(12)
   assertEquals("$27.00", viewModel.price.value)
}

Now if you run your test it should pass.

7. Solution code

8. Congratulations

In this codelab we:

  • Learned how to set up a LiveData test.
  • Learned how to test LiveData itself.
  • Learned how to test LiveData that is transformed.
  • Learned how to observe LiveData in a unit test.