Chapters

Hide chapters

Android Test-Driven Development by Tutorials

First Edition · Android 10 · Kotlin 1.3 · AS 3.5

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Testing on a New Project

Section 2: 8 chapters
Show chapters Hide chapters

Section III: TDD on Legacy Projects

Section 3: 9 chapters
Show chapters Hide chapters

8. Integration
Written by Victoria Gonda

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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 will test logic for a ViewModel from the Android Architecture Components. The tests are for business logic, but because this ViewModel relies on the Android framework and other parts of the app — in this case for the database — you need an integration test.

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.

Setting up the test class

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

class DetailViewModelTest {
}
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
// 1
private val wishlistDao: WishlistDao = Mockito.spy(
    Room.inMemoryDatabaseBuilder(
    InstrumentationRegistry.getInstrumentation().context,
    WishlistDatabase::class.java).build().wishlistDao())
// 2
private val viewModel = DetailViewModel(
    RepositoryImpl(wishlistDao))

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.

android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
}

dependencies {
  testImplementation 'org.robolectric:robolectric:4.2'
}
@RunWith(RobolectricTestRunner::class)
private val wishlistDao: WishlistDao = Mockito.spy(
      Room.inMemoryDatabaseBuilder(
      InstrumentationRegistry.getInstrumentation().context,
      WishlistDatabase::class.java)
          .allowMainThreadQueries()
          .build().wishlistDao())
mock-maker-inline

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())
}

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.saveWishlist(Wishlist("", listOf()))

Testing the wishlist’s save functionality

Repeat the TDD pattern for three more tests:

@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))
}

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

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())
}

return repository.getWishlist(0)

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

@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)
}
Attempt to invoke observeForever on a null object reference
Ogqurdy ro ucmotu urguthoLemowud os i lufy encesg ragabowvi

return repository.getWishlist(id)

Refactoring

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

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

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

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now