Chapters

Hide chapters

Android Test-Driven Development by Tutorials

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2.1

Section II: Testing on a New Project

Section 2: 8 chapters
Show chapters Hide chapters

Section III: TDD on Legacy Projects

Section 3: 8 chapters
Show chapters Hide chapters

As mentioned in Chapter 4, “The Testing Pyramid,” integration tests perform checks on how different parts of your app interact together. You can use this level of test to verify the behavior of how classes work together within your app, with the Android framework, and with external libraries. It’s the next level up from unit tests.

Unit tests are great for ensuring that all your individual pieces work. Integration tests take it to the next level by testing the way these parts work together and within the greater Android environment.

In this chapter, you’ll:

  • Learn what an integration test is and where are the best places to use them.
  • Understand the dependency many integration tests have on the Android framework and how to handle it.
  • Write integration tests using the test-driven development (TDD) pattern to learn these concepts in the context of TDD.

Getting started

To learn TDD with integration tests, you’ll work on a Wishlist app. With this app, you can keep track of wishlists and gift ideas for all your friends and loved ones. You will continue working on this app in the next chapter.

Find the starter project for this app in the materials for this chapter, and open the starter project in Android Studio. Build and run the app. You’ll see a blank screen with a button to add a list on the bottom. Clicking the button, you see a field to add a name for someone’s wishlist. Enter all you want right now, but it won’t save yet. You’ll be able to see it when you finish the implementation. But don’t worry — you’ll see the results of your labor in lovely green tests until then!

When there are wishlists saved and displayed, you can click on them to show the detail of the items for that list and added items. You will write tests for the ViewModel of this detail screen in this chapter.

In the end, this is what the app will look like:

Explore the files in the app for a moment. The ones you need to be familiar with in this chapter are:

  • DetailViewModel.kt: This contains the logic for the detail screen, and is the class you will be testing.
  • Repository.kt: This is the interface for the data repository in this app.
  • RepositoryImpl.kt: This is the implementation of the Repository interface.

When to use integration tests

Integration tests tend to be slower than unit tests, but quicker than UI tests. Because of this, you want to first put everything you can into unit tests. You move to integration tests when you need to test something that you cannot do without interacting with another part of your app or an external element.

Generally, when you want to create a unit test but can’t test it in isolation, you want to use an integration test. Sometimes you can get away with extracting the logic out so it can be unit tested (and is preferable), but the integration test is inevitable at times.

While balancing the lean towards more unit tests, you also need to rely on integration tests to make sure each of your thoroughly tested units works well together with others. If your database works perfectly, as does your view model, it’s still useless if the code linking them fails!

Testing with the Android framework

One of the most frequent dependencies that force you to use an integration test is the Android framework. This does not necessarily mean it uses the screen; it can be any component of the SDK. When your code ends up interacting with Android, you can’t get away with unit tests. For example, in this chapter, you’ll test the integration of the ViewModel down to the database. Because this test relies on other parts of the app, you need an integration test.

Once you implement the database in the next chapter, it will also rely on the Android framework. Because of that, you’ll write this test as if it uses the Android framework already.

Note: You may know that it is possible to test a ViewModel as a unit test. You saw an example in Chapter 7, “Introduction to Mockito.” For this exercise, you are specifically testing the ViewModel in an integration fashion — testing that it works with its dependencies rather than mocking them. If you want to see an example of how to test a similar ViewModel as a unit test, take a look at MainViewModelTest.kt in the project for this chapter.

When running tests that require the Android framework you have two options:

  1. Run them on an Android device or emulator.
  2. Use Robolectric.

Robolectric is a framework that allows you to run Android-dependent tests in a unit-test way. It creates a sandbox in which to run your tests; the sandbox acts like an Android environment with Android APIs. A benefit to using Robolectric is that it’s faster, running on the JVM. Using a device/emulator, however, more accurately shows how your code will behave when installed onto a device, having more Android features.

This chapter will show you how to use either a device/emulator or Robolectric. However, note that the final sample project is taking the emulator approach.

Creating a test class

Create a file called DetailViewModelTest.kt in the directory app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ wishlist. The key here is that it mimics the location of DetailViewModel.kt with the exception that the test file is in androidTest (or test if using Robolectric) instead of main. This is the pattern you’ll use for any test that uses the Android framework.

Using the Create Test shortcut

There’s also a shortcut to create test files as an alternative to manually creating them. Open DetailViewModel.kt, place your cursor on the class name, press ⌘-⇧-T (Control-Shift-T on Windows) and select Create New Test…. Android Studio then shows you a dialog to create this file for you.

Select Ok to accept the defaults, then, select the androidTest option (or test when using Robolectric for writing a unit test), and the editor does the rest.

See below:

Note: If you do not see the option to choose a destination directory, make sure app ‣ src ‣ androidTest ‣ java exists, then try again.

Setting up the test class

If it’s not there already, make sure you have the empty test class in your file:

class DetailViewModelTest {
}

Great! Now, make sure you have what you need to run your ViewModel test. Add the following test rule to your test class:

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

This is the same TestRule you used in Chapter 7, “Introduction to Mockito” to make sure that when you’re using LiveData with the ViewModel it’s all run synchronously in the tests.

Two more tasks before you’re able to write a test: 1. You need an instance of the DetailViewModel to perform the tests on; 2. You’ll need the dependency to create it. Add the following to your test class:

// 1
private val wishlistDao: WishlistDao =
    Mockito.spy(WishlistDaoImpl())
// 2
private val viewModel =
    DetailViewModel(RepositoryImpl(wishlistDao))

In the above, you:

  1. Create a spy of WishlistDao to use to create the Repository dependency for the DetailViewModel. You’re using Mockito to create the spy, as described in Chapter 7, “Introduction to Mockito.” You could mock the repository here, instead, but in this example, you will test their interaction. To see an example of what a test would look like with the Repository mocked, take a look at MainViewModelTest.kt.

  2. Create a DetailViewModel, with a RepositoryImpl created from your spy.

Using Robolectric

If you want to use Robolectric to run your tests, make sure your test is using test in the package structure instead of androidTest.

Note: The materials included for this chapter use the device/emulator approach rather than use Robolectric. All Robolectric steps are optional and are there to help you if you’re interested in using the library.

Then, add the following code within the andorid block in app ‣ build.gradle:

testOptions {
  unitTests.includeAndroidResources = true
}

This ensures that you can use Android resources in your unit tests.

Then, add the Robolectric dependency within the dependencies block in the same file:

testImplementation 'org.robolectric:robolectric:4.5-alpha-3'
testImplementation 'androidx.test.ext:junit:1.1.2'

This adds both the Robolectric and AndroidX Test APIs. As of Robolectric 4.0, Robolectic is compatible with these Android testing libraries, as you’ll see in a moment.

Note: You’re using an alpha version of Robolectric because Robolectric 4.4 only supports up to SDK 29, and at the time of this writing, Robolectric 4.5 is in alpha stages.

Gradle sync to apply your changes.

In DetailViewModelTest.kt, right above the class declaration add:

@RunWith(AndroidJUnit4::class)

This test runner will delegate to the appropriate runner to run Android tests. In this case, it will delegate to the RobolectricTestRunner.

Writing a failing integration test

In the fashion of TDD, write some tests before adding the implementation of DetailViewModel. Starting with the saveNewItem() function, write a test that verifies that saving a new item calls the database using the Data Access Object (DAO):

@Test
fun saveNewItemCallsDatabase() {
  // 1
  viewModel.saveNewItem(Wishlist("Victoria",
      listOf("RW Android Apprentice Book", "Android phone"), 1),
      "Smart watch")
  // 2
  verify(wishlistDao).save(any())
}

Here, you:

  1. Call the saveNewItem() function with data. You can use your name and wishes if you like!
  2. Verify that saveNewItem() called the save() function on the DAO using the same technique learned in Chapter 7, “Introduction to Mockito.”

This is an example of a “white-box test” that you learned about in Chapter 7, “Introduction to Mockito”.

Build and run your test to see it fail. You’ll need an emulator running or a device attached for the Android test to run on.

This error is saying that the save() function was never called on the wishlistDao. You know what you need to do to make it pass — keep moving on!

Making the test pass

Next step! The function needs to call save() on the DAO (and only call save()). Add the following to saveNewItem() in DetailViewModel:

repository.saveWishlistItem(Wishlist("", listOf()))

Run the test, and see it pass.

Testing the wishlist’s save functionality

Repeat the TDD pattern for three more tests:

  1. One similar to the above test that makes sure saveNewItem() saves the correct data.
  2. One that getWishList() calls the database using the DAO.
  3. One that ensures correct data returns when calling getWishList().

Add the first test to your test class:

@Test
fun saveNewItemSavesData() {
  // 1
  val wishlist = Wishlist("Victoria",
      listOf("RW Android Apprentice Book", "Android phone"), 1)
  // 2
  val name = "Smart watch"
  viewModel.saveNewItem(wishlist, name)

  // 3
  val mockObserver = mock<Observer<Wishlist>>()
  // 4
  wishlistDao.findById(wishlist.id)
      .observeForever(mockObserver)
  verify(mockObserver).onChanged(
      wishlist.copy(wishes = wishlist.wishes + name))
}

With this test, you:

  1. Create a new wishlist.
  2. Create a new item name for the list and call saveNewItem().
  3. Create a mock Observer to use as you’ve done in Chapter 7, “Introduction to Mockito.”
  4. Query the database and ensure that the wishlist you saved returns, signaling it saved correctly. When the program posts a value to a LiveData, the object calls onChanged() with the value. This is the function you are checking for.

It’s the same pattern: creating test data as your setup, calling the function, then verifying the result.

Build and run the test to make sure it fails before modifying the DetailViewModel to make it pass.

It’s the expected error you see. The function called save(), but it saved the wrong thing!

Once you see that it’s failing, change the body of saveNewItem() to make it pass:

repository.saveWishlistItem(
    wishlist.copy(wishes = wishlist.wishes + name))

Note: You may be wondering why you’re making a copy of the wishlist here instead of mutating it. This is to follow the safety of immutability. You can learn more about this in “Functional Programming for Android Developers” https://medium.freecodecamp.org/functional-programming-for-android-developers-part-2-5c0834669d1a.

Run it again to see it pass.

Testing the database queries

Add the next test to ensure that getWishlist() calls the database:

@Test
fun getWishListCallsDatabase() {
  viewModel.getWishlist(1)

  verify(wishlistDao).findById(any())
}

This looks very similar to your first test in this class. Run it and see it fail.

There’s the message that says you need to implement that call to the wishlistDao.

Once you’ve run your failing test, change the code in getWishList() to make it pass:

return repository.getWishlist(0)

Run all the tests that you’ve written in this chapter. Hooray! All three tests are passing!

Testing the data returned

Your last test in this chapter is to make sure getWishlist() returns the correct data. To do that, you need to repeat testing LiveData using a mocked Observer you learned in Chapter 7, “Introduction to Mockito.”

Add this final test to your test class:

@Test
fun getWishListReturnsCorrectData() {
  // 1
  val wishlist = Wishlist("Victoria",
      listOf("RW Android Apprentice Book", "Android phone"), 1)
  // 2
  wishlistDao.save(wishlist)
  // 3
  val mockObserver = mock<Observer<Wishlist>>()
  viewModel.getWishlist(1).observeForever(mockObserver)
  // 4
  verify(mockObserver).onChanged(wishlist)
}

Taking each part in turn:

  1. Again, you set up your test data.
  2. Save a wishlist to the database to be retrieved later in this test.
  3. Create a mockObserver, mocking a lifecycle Observer. You use this observer to observeForever() on the LiveData that getWishlist() returns.
  4. Verify that the function published the correct data.

Run your test to make sure it fails.

Attempt to invoke observeForever on a null object reference
Attempt to invoke observeForever on a null object reference

This error says that getWishlist() is returning null! Sure, you mocked it, but only when you use an id of 1, the id you’re passing into getWishlist(). If you look in the DetailViewModel, right now there is a 0 hardcoded in for the id. Change the body of the getWishlist() function to use the id that’s passed in:

return repository.getWishlist(id)

Run the test again. You can do this by clicking on the Run Test button near the name of the test class.

All green! Great job!

Refactoring

Now that you have green tests for DetailViewModel and how it interacts with LiveData and the database, you can refactor with confidence.

Take a look at saveNewItem() in DetailViewModel. It’s doing a bit of work to format the Wishlist for saving:

fun saveNewItem(wishlist: Wishlist, name: String) {
  repository.saveWishlistItem(
      wishlist.copy(wishes = wishlist.wishes + name))
}

One could argue that this responsibility belongs to the repository. Why not perform that refactoring!

There are three files you need to change to refactor this, and it won’t compile until you’ve done all three.

  1. In DetailViewModel change the contents of saveNewItem() to be:
repository.saveWishlistItem(wishlist, name)
  1. In the Repository interface, change the saveWishlistItem() signature to this:
fun saveWishlistItem(wishlist: Wishlist, name: String)
  1. In RepositoryImpl, change the body of saveWishlistItem() to have the logic you deleted from the DetailViewModel, and the signature to match the interface:
override fun saveWishlistItem(
  wishlist: Wishlist,
  name: String
) {
  wishlistDao.save(
    wishlist.copy(wishes = wishlist.wishes + name))
}

Run the tests to make sure nothing broke during the refactor.

It didn’t — congratulations!

Running the app

After all your hard work, you can see your app in action! Build and run the app, and play around with creating wishlists. In the next chapter, you’ll be able to add items.

Key points

  • Integration tests verify the way different parts of your app work together.
  • They are slower than unit tests, and should therefore only be used when you need to test how things interact.
  • When interacting with the Android framework you can rely on an Android device or emulator, or use Robolectric.
  • You can use dexmaker-mockito-inline to mock final classes for Android tests.

Where to go from here?

You can find the final version of the code in this chapter in the chapter materials.

If you want to continue exploring integration tests in this app, take a look at MainViewModelTest.kt at the tests already written there.

There’s much more that you can do with integration tests than ViewModel tests. In Chapter 9, “Testing the Persistence Layer,” you’ll learn how you can test your persistence layer, and Chapter 10, “Testing the Network Layer,” introduces the network layer.

For more about integration testing, you can look at the Android documentation:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.