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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.

@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!

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

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.