Testing Android Architecture Components
Learn how to test the Architecture Components library included in the Android Jetpack suite released in 2017 by Google’s Android Team. By Enzo Lizama.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Testing Android Architecture Components
20 mins
- Getting Started
- What Are Android Architecture Components?
- Structuring Your Tests
- Testing on Android
- Testing ViewModel and LiveData
- Verifying onChanged() Events
- Asserting LiveData Values
- Testing DAO
- Testing Room Migrations
- Creating Your Room Migration Test
- Testing Incremental Migration
- Testing All Migrations
- Where to Go From Here?
At the 2017 Google I/O event, the Android team released Android Jetpack, a suite of libraries to help developers follow best practices, reduce boilerplate code and write code that works consistently across Android versions and devices.
Since then, Android developers have adopted those libraries to increase their productivity and their code quality, but testing using the new libraries is complicated. In this tutorial, you’ll learn how to test Android Jetpack’s Architecture Components libraries.
You’ll take a deep look into Android Architecture Components testing techniques by working on RWQuotes, a simple app that allows you to create and read famous quotes in local storage. In the process, you’ll learn about:
- Basic testing concepts
- The testing tools available for Android
- How to test ViewModel and LiveData
- Testing Room database operations and migrations
Now, you’ll get started by taking a look at the project.
Getting Started
Download the materials for this tutorial by clicking the Download Materials button at the top or bottom of this tutorial page. Extract the ZIP file and open the start project in Android Studio. Please check that you have the latest stable version.
Once the Gradle tasks complete, build and run and you’ll see RWQuotes’ home screen.
Here’s a look at the project structure:
Inside the data folder, you’ll find:
-
Quote.kt: A data class that represents the
entity
you’ll store in the database. -
QuoteDao.kt: An abstract class where you define the database interactions with annotations like
@Query
,@Insert
,@Delete
and@Update
. - Migrations.kt: Stores the migration values from different versions.
- RWQuotesDatabase.kt: Creates the Room database, allows you to get the instance for that, and manages the migrations and the prepopulated data.
- QuoteRepositoryImpl.kt: Implements QuoteRepository.kt and uses QuoteDao.kt to perform the CRUD operations.
Next, you have ui, which stores everything related to the views and user interactions. Within that folder, you’ll find >ui/viewmodel, which contains everything related to LiveData
and ViewModel
. The data request for QuoteRepository.kt is here.
Finally, you’ll see RWQuoteApplication.kt, which is an application class that initializes the debugger when you’re in develop mode.
So now that you have a good idea of how the project is set up, it’s time to take a moment to understand the architecture components you’ll work with throughout this tutorial.
What Are Android Architecture Components?
Android architecture components are a collection of libraries that help you design robust, testable and maintainable apps.
In this tutorial, you’ll use the following classes to manage your UI component lifecycle and handle data persistence:
- LiveData helps you build data objects that notify views when the underlying database changes.
- ViewModel stores UI-related data that isn’t destroyed on app rotations.
- Room is an SQLite object mapping library. Use it to avoid boilerplate code and easily convert SQLite table data to Java objects. Room provides compile time checks of SQLite statements and can return RxJava, Flowable and LiveData observables.
Structuring Your Tests
Users interact with apps on a variety of levels, from pressing a button to downloading information onto their device. To make sure that every function works, you should test a variety of use cases and interactions as you iteratively develop your app.
The image below shows the Testing Pyramid, showing the three categories of tests you should include in your app’s test suite:
As you work up the pyramid from small tests to large tests, each test increases fidelity but also increases in execution time and effort to maintain and debug. Therefore, you should write more unit tests than integration tests, and more integration tests than UI tests.
Testing on Android
By default, when you create a new Android project on Android Studio, it will create two folders for testing: androidTest and test. androidTest contains tests that run on real or virtual devices. These include integration tests, end-to-end tests and other tests where the JVM alone cannot validate your app’s functionality.
test
contains tests that run on your local machine, such as unit tests.
There are many libraries that simplify the testing process. Here are the most popular for Android development testing:
- JUnit: The most popular and widely-used unit testing framework for Java.
- Mockito: Helps you configure mock objects to return specific values when you invoke them.
- Espresso: Use Espresso to write concise, beautiful and reliable Android UI tests.
- Robolectric: A framework that brings fast and reliable unit tests to Android. Tests run inside the JVM on your workstation in seconds.
If you’re unfamiliar with them, check out these tutorials:
- Android Unit Testing with Mockito
- Espresso Testing and Screen Robots: Getting Started
- Test-Driven Development Tutorial for Android: Getting Started
For this tutorial, you’ll make unit tests for ViewModel
and LiveData
and integration tests for the Room database. These tests have something in common: You’ll use a special Rule, InstantTaskExecutorRule
, to make architecture component testing easy as possible.
Here’s how it looks:
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
This is a JUnit test rule that swaps the background executor used by the architecture components with a different one that executes each task synchronously. This rule is commonly used for host-side tests that use architecture components. You’ll use this rule going forward to make it easier to create tests.
Now that you know some useful concepts about Android testing, it’s time to get started creating your test.
Testing ViewModel and LiveData
Start by creating a new test class by right-clicking on the test package and selecting New ▸ Kotlin File/Class. Name the new class QuotesViewModelTest
and add the following to it:
// 1
@RunWith(AndroidJUnit4::class)
class QuotesViewModelTest {
// 2
@Mock
private lateinit var viewModel: QuotesViewModel
@Mock
private lateinit var isLoadingLiveData: LiveData<Boolean>
@Mock
private lateinit var observer: Observer<in Boolean>
// 3
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// 4
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
viewModel = spy(QuotesViewModel(ApplicationProvider.getApplicationContext(),
QuotesRepositoryImpl(ApplicationProvider.getApplicationContext())))
isLoadingLiveData = viewModel.dataLoading
}
// Your tests go here ...
}
Before starting to create your tests, you need to set everything up to make the tests work as expected. So, here’s what you do in the code above:
- You need to annotate the class to run with
AndroidJUnit4
, which is a cross-environment JUnit4 runner for Android tests. This implementation will delegate to the appropriate runner based on the value the build system provides. - Mark the necessary fields as
@Mock
to allow shorthand mock creation and minimize repetitive mock creation code. - Define the
InstantTaskExecutorRule
to run any executors synchronously - Finally, in the
setup
function marked as@Before
, you initialize the mocks for the tests and assign values for others. The following tests have to go below this code.
Next, you’ll write a test that verifies that the progress state triggers within your ViewModel.
Verifying onChanged() Events
In QuotesViewModelTest
, add a new test to the bottom of the class:
/**
* Testing *onChanged()* method for [LiveData]
*
*/
@Test
fun `Verify livedata values changes on event`() {
assertNotNull(viewModel.getAllQuotes())
viewModel.dataLoading.observeForever(observer)
verify(observer).onChanged(false)
viewModel.getAllQuotes()
verify(observer).onChanged(true)
}
In this code, you use the verify
method from the Mockito library to ensure the behavior happened once. You also call onChanged
when the data changes for your dataLoading
value. This is a good example of showing how changes in your code can be verified after each method call.
In the next section, you’ll look at how to unit test LiveData objects.
Asserting LiveData Values
For these tests, you have to verify the values inside the LiveData
instances in the tests. At the bottom of QuotesViewModelTest
, add the following tests.
/**
* Test asserting values for [LiveData] items on [QuotesViewModel] to insert [Quote]
*
*/
@Test
fun `Assert loading values are correct fetching quotes`() {
// 1
val testQuote = Quote(id = 1, text = "Hello World!", author = "Ray Wenderlich",
date = "27/12/1998")
// 2
var isLoading = isLoadingLiveData.value
// 3
assertNotNull(isLoading)
// 4
isLoading?.let { assertTrue(it) }
// 5
viewModel.insertQuote(testQuote)
// 6
isLoading = isLoadingLiveData.value
assertNotNull(isLoading)
isLoading?.let { assertFalse(it) }
}
/**
* Test asserting values for [LiveData] items on [QuotesViewModel] to delete [Quote]
*
*/
@Test
fun `Assert loading values are correct deleting quote`() {
// 1
val testQuote = Quote(id = 1, text = "Hello World!", author = "Ray Wenderlich",
date = "27/12/1998")
// 2
var isLoading = isLoadingLiveData.value
// 3
assertNotNull(isLoading)
// 4
isLoading?.let { assertTrue(it) }
// 5
viewModel.delete(testQuote)
// 6
isLoading = isLoadingLiveData.value
assertNotNull(isLoading)
isLoading?.let { assertFalse(it) }
}
/**
* Test asserting values for [LiveData] items on [QuotesViewModel] to update [Quote]
*
*/
@Test
fun `Assert loading values are correct updating quote`() {
// 1
val testQuote = Quote(id = 1, text = "Hello World!", author = "Ray Wenderlich",
date = "27/12/1998")
// 2
var isLoading = isLoadingLiveData.value
// 3
assertNotNull(isLoading)
// 4
isLoading?.let { assertTrue(it) }
// 5
viewModel.updateQuote(testQuote)
// 6
isLoading = isLoadingLiveData.value
assertNotNull(isLoading)
isLoading?.let { assertFalse(it) }
}
The code above follows these steps:
- Defines a test instance of
Quote
. - Gets the value of
isLoadingLiveData
. - Asserts that it’s not
null
to avoid comparing null values. - After that, checks if the value matches what’s expected. For this case, the value has to be
true
. - Then performs the DAO operation for the test.
- Finally, makes the reverse check of values for
isLoadingLiveData
.
When you finish, run the test inside QuotesViewModelTest
and check your results.
All the tests pass! With the Live Data objects tested, let’s move onto testing the room database.
Testing DAO
Room Database provides a Data Access Object (DAO) to access your app’s data. This set of objects forms the main component of Room. Each DAO includes methods that offer abstract access to your database app.
In the sample project, you can find definitions for all the CRUD operations at data/QuoteDao.kt. Open up the file in the project and take a look at the queries.
@Dao
interface QuotesDao {
@Query("SELECT * FROM rwquotes ORDER BY id DESC")
fun getQuotes(): LiveData<List<Quote>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertQuote(quote: Quote) : Long
@Update
fun updateQuote(quote: Quote): Int
@Delete
fun deleteQuote(quote: Quote): Int
}
You’re going to create tests for these queries. Create a new test class by right-clicking on the androidTest package and selecting New ▸ Kotlin File/Class. Name it DatabaseTest.
Next, copy the following code into the class.
@RunWith(AndroidJUnit4::class)
abstract class DatabaseTest {
protected lateinit var appDatabase: RWQuotesDatabase
@Before
fun initDb() {
appDatabase = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
RWQuotesDatabase::class.java)
.allowMainThreadQueries()
.build()
}
@After
@Throws(IOException::class)
fun closeDb() {
appDatabase.close()
}
}
Let’s go through the code. To test the DAO operations, you need to check if all the operations work with the database as expected. To do that, you create a database just for testing purposes.
Room offers a solution with inMemoryDatabaseBuilder
, which creates RoomDatabase.Builder as an in-memory database. Information stored in an in-memory database disappears when the process finishes.
Finally, you define an abstract class to initialize the in-memory database before the test starts executing and to close that database when the test is over.
Next, create a new class called QuoteDaoTest
inside the androidTest package. Then, paste the following code into the file.
@RunWith(AndroidJUnit4::class)
open class QuoteDaoTest : DatabaseTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun insertQuoteTest() {
val quote = Quote(id = 1, text = "Hello World", author = "Ray Wenderlich", date = "27/12/1998")
appDatabase.quotesDao().insertQuote(quote)
val quotesSize = appDatabase.quotesDao().getQuotes().getValueBlocking()?.size
assertEquals(quotesSize, 1)
}
@Test
fun deleteQuoteTest() {
val quote = Quote(id = 1, text = "Hello World", author = "Ray Wenderlich", date = "27/12/1998")
appDatabase.quotesDao().insertQuote(quote)
assertEquals(appDatabase.quotesDao().getQuotes().getValueBlocking()?.size, 1)
appDatabase.quotesDao().deleteQuote(quote)
assertEquals(appDatabase.quotesDao().getQuotes().getValueBlocking()?.size, 0)
}
@Test
fun getQuoteAsLiveDataTest() {
val quote = Quote(id = 1, text = "Hello World", author = "Ray Wenderlich", date = "27/12/1998")
appDatabase.quotesDao().insertQuote(quote)
val quoteLiveDataValue = appDatabase.quotesDao().getQuotes().getValueBlocking()
assertEquals(quoteLiveDataValue?.size, 1)
}
@Test
fun updateQuoteTest() {
var quote = Quote(id = 1, text = "Hello World", author = "Ray Wenderlich", date = "27/12/1998")
appDatabase.quotesDao().insertQuote(quote)
quote.author = "Enzo Lizama"
appDatabase.quotesDao().updateQuote(quote)
assertEquals(appDatabase.quotesDao().getQuotes().getValueBlocking()?.get(0)?.author, "Enzo " +
"Lizama")
}
}
For each operation, you need to verify that the action completes successfully. So QuoteDaoTest
needs to extend from DatabaseTest
to execute the database initialization previous from tests. For testing purposes, the test will assert the size value for most cases like insert, delete, and read.
For the update, the test will assert the value that is going to update is expected. Insert the below code to understand better.
Now, run your tests and you’ll get a successful result. Well done!
In the next section, you’ll see how to test database migrations.
Testing Room Migrations
When you add functionalities to the app or modify them, you need to modify your Room entity classes to reflect these changes.
Migrations are often complex, and an incorrectly-defined migration could cause your app to crash. To preserve your app’s stability, you should test your migrations.
Room provides a room-testing Maven artifact to assist with the testing process. However, for this artifact to work, you must first export your database’s schema.
Open the app build.gradle
file. Then within the android
brackets, paste the following code.
// app/build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
This lets you export your database schema into a JSON file at compile time.
To export the schema, set the room.schemaLocation annotation processor property in app/build.gradle:
android {
...
sourceSets {
// Adds exported schema location into the app assets
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
Here, you add the database schemas to the project directory in a folder called schemas
.
Additionally, to test your migrations you must add the androidx.room:room-testing Maven artifact from Room into your test dependencies and add the location of the exported schema as an asset folder. If you look in the dependencies
block of the gradle file, you will see this already added to the project.
The exported JSON files represent your database’s schema history. Store these files in your version control system because it allows Room to create older versions of the database for testing purposes.
The room-testing library provides MigrationTestHelper
, which can read exported schema files. Its package also implements the JUnit4 TestRule interface, which lets it manage created databases.
Great, with the json files available to your project. The next step is to create a migration test for Room.
Creating Your Room Migration Test
Create a new test class by right-clicking on the androidTest package, select New ▸ Kotlin File/Class and name the new class RWQuoteMigrationTest
. Then, add the following code to it:
@RunWith(AndroidJUnit4::class)
class RWQuoteMigrationTest {
private lateinit var database: SupportSQLiteDatabase
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
companion object {
private const val TEST_DB = "migration-test"
}
@get:Rule
val migrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
RWQuotesDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
// Your test goes here ...
}
In this class, you need to define a new Rule
to make the testing process possible. MigrationTestHelper
creates a new migration helper. It uses the Instrumentation context to load the schema — which falls back to the app resources — and the target context to create the database.
In the next section, you’ll begin to test a room migration.
Testing Incremental Migration
In the project, open data/Migrations.kt
.
@VisibleForTesting
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rwquotes ADD COLUMN 'stars' INTEGER NOT NULL DEFAULT 0")
}
}
The above migration represents the difference between the first and second version of the same Entity
for the database. For this example, the entity is Quote
.
It represents that the second version of the table rwquotes
has stars
as a new attribute. So, to avoid incompatibilities between versions, you have to do a migration.
Back in RWQuoteMigrationTest
. Add the following test:
@Test
fun migrate1to2() {
// 1
database = migrationTestHelper.createDatabase(TEST_DB, 1).apply {
execSQL(
"""
INSERT INTO rwquotes VALUES (10, 'Hello', 'Shakespeare', '12/12/21')
""".trimIndent()
)
close()
}
// 2
database = migrationTestHelper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
// 3
val resultCursor = database.query("SELECT * FROM rwquotes")
// Make sure you can find the age column, and assert it's equal to the default.
// You can also validate the name is the one you inserted.
assertTrue(resultCursor.moveToFirst())
// 4
val authorColumnIndex = resultCursor.getColumnIndex("author")
val textColumnIndex = resultCursor.getColumnIndex("text")
val authorFromDatabase = resultCursor.getString(authorColumnIndex)
val textFromDatabase = resultCursor.getString(textColumnIndex)
assertEquals("Shakespeare", authorFromDatabase)
assertEquals("Hello", textFromDatabase)
}
To verify the migration is successful, you have to test it. To achieve this, you:
- Set up
runMigrationsAndValidate
, which runs the given set of migrations on the provided database. - After the migration, the method validates the database schema to ensure that the migration result matches the expected schema.
- When the validation succeeds, you ensure that inserted values are the expected ones, making a query to retrieve all the values for the database located in memory.
- Finally, you get the indexes for the columns of
author
andtext
and check if the values are what you expected after the assertion.
Good job. That’s one migration of your database tests. In the next section, you’ll learn how to test multiple migrations.
Testing All Migrations
In the step above, you tested the migration from two different versions, but in the real world, you’d have many more versions to test. Therefore, it’s highly recommended to include a test that covers all the migrations defined in your database. This ensures there are no problems between a newer database instance and an older one that follows the migrations.
Add the following test to the bottom of RWQuoteMigrationTest
.
private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3)
@Test
@Throws(IOException::class)
fun migrateAll() {
// 1
migrationTestHelper.createDatabase(TEST_DB, 2).apply {
close()
}
// 2
Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
RWQuotesDatabase::class.java,
TEST_DB
).addMigrations(*ALL_MIGRATIONS).build().apply {
openHelper.writableDatabase
close()
}
}
To test all the migrations for the local database, follow these steps:
- Create the earliest version of the database.
- Open the latest version of the database. Room will validate the schema once all migrations execute.
So now you can run your tests and expect a successful result.
All tests are green. Looking good!
Where to Go From Here?
You’ve just learnt how to unit test commonly used architecture components in Android.
You can download the completed project files by using the Download Materials button at the top or bottom of the tutorial.
If you’re more curious about Android topics, take a look into the Android developer docs for more info.
We hope you enjoyed this tutorial. If you have any questions, please join the discussion below.