Saltar al contenido

ᐉ Testing Android Architecture Components [FREE] ᐉ New Mobile Gadget

noviembre 16, 2022

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.

Note: This tutorial assumes you know how to build an Android app and are familiar with the Kotlin language. If you’re not, check out our Android beginner path.

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.

RWQuotes home page with two quotes displayed

Here’s a look at the project structure:

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:

A pyramid containing three layers: unit tests at the bottom, integration tests in the middle, and UI tests at the top

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:

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:Ruleval 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 @Mock private lateinit var observer: Observer // 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:

  1. 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.
  2. Mark the necessary fields as @Mock to allow shorthand mock creation and minimize repetitive mock creation code.
  3. Define the InstantTaskExecutorRule to run any executors synchronously
  4. 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:

  1. Defines a test instance of Quote.
  2. Gets the value of isLoadingLiveData.
  3. Asserts that it’s not null to avoid comparing null values.
  4. After that, checks if the value matches what’s expected. For this case, the value has to be true.
  5. Then performs the DAO operation for the test.
  6. Finally, makes the reverse check of values for isLoadingLiveData.

When you finish, run the test inside QuotesViewModelTest and check your results.

QuotesViewModel Test 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.

@Daointerface QuotesDao { @Query("SELECT * FROM rwquotes ORDER BY id DESC") fun getQuotes(): LiveData<List> @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!

Test results showing positive results for deleteQuoteTest, updateQuoteTest, getQuoteAsLiveDataTest and insertQuoteTest

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.gradleandroid { ... 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.

Room schema screenshot

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.

@VisibleForTestingval 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:

  1. Set up runMigrationsAndValidate, which runs the given set of migrations on the provided database.
  2. After the migration, the method validates the database schema to ensure that the migration result matches the expected schema.
  3. 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.
  4. Finally, you get the indexes for the columns of author and text 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:

  1. Create the earliest version of the database.
  2. 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.

Test results showing a successful migration

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.

Download Materials

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!