1. Before you begin
In previous codelabs you learned how to use ViewModel
s 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
ViewModel
s andLiveData
.
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
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- 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.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
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.
- To specify that
LiveData
objects should not call the main thread we need to provide a specific test rule any time we are testing aLiveData
object.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
- Now we can create a function called
quantity_twelve_cupcakes()
. In the method, create an instance of theOrderViewModel.
- In this test, you will be checking to make sure the
quantity
object in theOrderViewModel
is updated whensetQuantity
is called. But before calling any methods or working with any data in theOrderViewModel
, it is important to note that when testing the values of aLiveData
object, the objects need to be observed in order for changes to be emitted. A simple way of doing this is by using theobserveForever
method. Call theobserveForever
method on thequantity
object. This method requires a lambda expression, but that can be left empty. - Then call the
setQuantity()
method, passing in12
as a parameter.
val viewModel = OrderViewModel()
viewModel.quantity.observeForever {}
viewModel.setQuantity(12)
- We can safely infer that the value of the
quantity
object is12
. Note thatLiveData
objects are not the value itself. Values are contained in a property calledvalue
. 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.
- Create a method called
price_twelve_cupcakes()
and annotate it as a test. - In the method, create an instance of the
OrderViewModel
and call thesetQuantity()
method, passing in 12 as a parameter.
val viewModel = OrderViewModel()
viewModel.setQuantity(12)
- Looking at the
PRICE_PER_CUPCAKE
inOrderViewModel
, we can see that cupcakes are $2.00 each. We can also see thatresetOrder()
is called every time theViewModel
is initialized, and in this method, the default date is today's date, andPRICE_FOR_SAME_DAY_PICKUP
is $3.00. Therefore, 12 * 2 + 3 = 27. We expect that the value of theprice
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 theprice LiveData
object.
assertEquals("$27.00", viewModel.price.value)
Now run the test.
It should fail!
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.
- 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.