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

9. Testing the Persistence Layer
Written by Victoria Gonda

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In most apps you’ll build, you will store data in one way or another. It might be in shared preferences, in a database, or otherwise. No matter which way you’re saving it, you need to be confident it is always working. If a user takes the time to put together content and then loses it because your persistence code broke, both you and your user will have a sad day.

You have the tools for mocking out a persistence layer interface from Chapter 7, “Introduction to Mockito.” In this chapter you will take it a step further, testing that when you interact with a database, it behaves the way you expect.

In this chapter you will learn:

  • How to use TDD to have a well-tested RoomDB database.
  • Why persistence testing can be difficult.
  • Which parts of your persistence layer should you test.

Note: It is helpful, but not necessary to have a basic understanding of RoomDB. To brush up on the basics, check out our tutorial, “Data Persistence with Room”: https://www.raywenderlich.com/69-data-persistence-with-room.

Getting started

To learn about testing the persistence layer you will write tests while building up a RoomDB database for the Wishlist app. This app provides a place where you can keep track of the wishlists and gift ideas for all your friends and loved ones.

To get started, find the starter project included for this chapter and open it up in Android Studio, or continue with the project from Chapter 8, “Integration.”

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. You can add a name, but it will be gone next time you open the app! You will implement the persistence layer to save the wishlist in this chapter.

When there are wishlists saved and displayed, you can click on them to show the detail of the items for that list, and add items. By the end of this chapter, this is what the app will look like:

Time to get familiar with the code.

Exploring the project

There are a couple of files you should be familiar with before getting started. Open these files and take a look around:

Setting up the test class

As with any test, the first thing you need to do is create the file. Create WishlistDaoTest.kt in app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ wishlist ‣ persistence. In it, create an empty class with the @RunWith annotation. The import you want for AndroidJUnit4 is androidx.test.ext.junit.runners.AndroidJUnit4:

@RunWith(AndroidJUnit4::class)
class WishlistDaoTest {
}
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var wishlistDatabase: WishlistDatabase
private lateinit var wishlistDao: WishlistDao

Setting up the database

Right now, the WishlistDao has a fake implementation so it would compile for the previous chapter. Because you want to use a real DAO with a real database in this chapter, you need to set that up. Take a moment to make the following changes. Note that the app will not compile until all the following steps are complete.

abstract fun wishlistDao(): WishlistDao

Using an in-memory database

One of the challenges that make writing persistence tests difficult is managing the state before and after the tests run. You’re testing saving and retrieving data, but you don’t want to end your test run with a bunch of test data on your device or emulator. How can you save data while your tests are running, but ensure that test data is gone when the tests finish? You could consider erasing the whole database, but if you have your own non-test data saved in the database outside of the tests, that would delete too.

@Before
fun initDb() {
  // 1
  wishlistDatabase = Room.inMemoryDatabaseBuilder(
      ApplicationProvider.getApplicationContext(),
      WishlistDatabase::class.java).build()
  // 2
  wishlistDao = wishlistDatabase.wishlistDao()
}
@After
fun closeDb() {
  wishlistDatabase.close()
}

Writing a test

Test number one is going to test that when there’s nothing saved, getAll() returns an empty list. This is a function for fetching all of the wishlists from the database. Add the following test, using the imports androidx.lifecycle.Observer for Observer, and org.mockito.kotlin.* for mock() and verify():

@Test
fun getAllReturnsEmptyList() {
  val testObserver: Observer<List<Wishlist>> = mock()
  wishlistDao.getAll().observeForever(testObserver)
  verify(testObserver).onChanged(emptyList())
}
An abstract DAO method must be annotated with one and only one of the following annotations
Ux iqwrcenp GUE kuljug kals be ijbamimiv tomt odi ixc uddq exu ug glo riygoleyz isbudimeimq

@Query("")
Must have exactly 1 query in the value of @Query
Fivs sero edaqlwx 0 vaibf iq cgo leloi as @Seank

@Query("SELECT * FROM wishlist")
@Query("SELECT * FROM wishlist WHERE id != :id")
fun findById(id: Int): LiveData<Wishlist>

@Delete
fun save(vararg wishlist: Wishlist)

Knowing not to test the library

The fact that RoomDB made it hard to write a failing test is a clue. When your tests align closely with a library or framework, you want to be sure you’re testing your code and not the third-party code. If you really want to write tests for that library, you might be able to contribute to the library, if it’s an open-source project. ;]

Testing an insert

With any persistence layer, you need to be able to save some data and retrieve it. That’s exactly what your next test will do. Add this test to your class:

@Test
fun saveWishlistsSavesData() {
  // 1
  val wishlist1 = Wishlist("Victoria", listOf(), 1)
  val wishlist2 = Wishlist("Tyler", listOf(), 2)
  wishlistDao.save(wishlist1, wishlist2)

  // 2
  val testObserver: Observer<List<Wishlist>> = mock()
  wishlistDao.getAll().observeForever(testObserver)

  // 3
  val listClass =
      ArrayList::class.java as Class<ArrayList<Wishlist>>
  val argumentCaptor = ArgumentCaptor.forClass(listClass)
  // 4
  verify(testObserver).onChanged(argumentCaptor.capture())
  // 5
  assertTrue(argumentCaptor.value.size > 0)
}
@Delete
fun save(vararg wishlist: Wishlist)
AssertionFailedError
UgpavkaigSoixapAtdev

Making your test pass

This one is simple enough to make it pass. Just change the @Delete annotation with an @Insert. Your save() signature should now look like this:

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun save(vararg wishlist: Wishlist)

Testing your query

Now that you have a way to save data in your database, you can test your getAll() query for real! Add this test:

@Test
fun getAllRetrievesData() {
  val wishlist1 = Wishlist("Victoria", emptyList(), 1)
  val wishlist2 = Wishlist("Tyler", emptyList(), 2)
  wishlistDao.save(wishlist1, wishlist2)

  val testObserver: Observer<List<Wishlist>> = mock()
  wishlistDao.getAll().observeForever(testObserver)

  val listClass =
      ArrayList::class.java as Class<ArrayList<Wishlist>>
  val argumentCaptor = ArgumentCaptor.forClass(listClass)
  verify(testObserver).onChanged(argumentCaptor.capture())
  val capturedArgument = argumentCaptor.value
  assertTrue(capturedArgument
      .containsAll(listOf(wishlist1, wishlist2)))
}

Fixing the bug

How could this happen? StringListConverter holds the key. Take a look at the object. In stringToStringList(), when there is an empty String saved in the database, as is the case for an empty list, the split function used returns a list with an empty string in it! Now that you know the problem, you can solve it.

if (!string.isNullOrBlank()) string.split("|").toMutableList()
else mutableListOf()

Testing a new query

Moving on. In your database you also need the ability to retrieve an item by id. To create this functionality, start by adding a test for it:

@Test
fun findByIdRetrievesCorrectData() {
  // 1
  val wishlist1 = Wishlist("Victoria", emptyList(), 1)
  val wishlist2 = Wishlist("Tyler", emptyList(), 2)
  wishlistDao.save(wishlist1, wishlist2)
  // 2
  val testObserver: Observer<Wishlist> = mock()
  wishlistDao.findById(wishlist2.id).observeForever(testObserver)
  verify(testObserver).onChanged(wishlist2)
}
@Query("SELECT * FROM wishlist WHERE id != :id")
fun findById(id: Int): LiveData<Wishlist>
Arguments are different
Evjademfy iba fuxyedirg

Making the test pass

It’s the last time you’ll do it in this chapter: make that test green! All you need to do is remove that not (!) from the query. It should now look like this:

@Query("SELECT * FROM wishlist WHERE id = :id")

Creating test data

You have a working database with reliable tests but there’s more that you can do. There is something you can do to also help save setup time as you write other tests. This tool is called test data creation.

object WishlistFactory {
}
// 1
private fun makeRandomString() = UUID.randomUUID().toString()
// 2
private fun makeRandomInt() =
    ThreadLocalRandom.current().nextInt(0, 1000 + 1)
fun makeWishlist(): Wishlist {
  return Wishlist(
      makeRandomString(),
      listOf(makeRandomString(), makeRandomString()),
      makeRandomInt())
}

Using a Factory in your test

You now have an easy way to create test data, so why not use it? Refactor your tests so that each time you create a Wishlist, you use the factory instead. It should look like this in each of your tests:

val wishlist1 = WishlistFactory.makeWishlist()
val wishlist2 = WishlistFactory.makeWishlist()

Hooking up your database

You now have beautiful, tested database interactions, so surely you want to see them in action! Before you run the app, open up KoinModules.kt and change single<WishlistDao> { WishlistDaoImpl() } to:

single {
  Room.databaseBuilder(
      get(),
      WishlistDatabase::class.java, "wishlist-database"
  )
      .allowMainThreadQueries()
      .build().wishlistDao()
}

Optional: Updating your integration test

The integration test, DetailViewModelTest, that you wrote in the last chapter is still using the fake DAO implementation. Wouldn’t it be great to use the real one and delete the fake one?

private val wishlistDao: WishlistDao = Mockito.spy(
    Room.inMemoryDatabaseBuilder(
        ApplicationProvider.getApplicationContext(),
        WishlistDatabase::class.java)
        .allowMainThreadQueries()
        .build().wishlistDao())

Using Dexmaker

One final thing. The WishlistDao implementation that RoomDB provides is a final class, which means you can’t spy on it using Mockito. While in Chapter 7, “Introduction to Mockito” you used the mock-maker-inline extension, you cannot use that in Android tests.

androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.1'
androidTestImplementation 'org.mockito:mockito-android:3.10.0'

Handling stateful tests

In this chapter, you learned hands-on how to handle the statefulness of your tests using an in-memory database. You need this set up and tear down to write reliable, repeatable persistence tests, but how do you handle it when you’re using something other than RoomDB for your persistence?

Key points

  • Persistence tests help keep your user’s data safe.
  • Statefulness can make persistence tests difficult to write.
  • You can use an in-memory database to help handle stateful tests.
  • You need to include both set up (@Before) and tear down (@After) with persistence tests.
  • Be careful to test your code and not the library or framework you’re using.
  • Sometimes you need to write ”broken” code first to ensure that your tests fail.
  • You can use Factories to create test data for reliable, repeatable tests.
  • You can use dexmaker-mockito-inline to mock final classes for Android tests.
  • If the persistence library you’re using doesn’t have built-in strategies for testing, you may need to delete all persisted data before and after each test.

Where to go from here?

You now know how to get started testing your persistence layer in your app. Keep these strategies in mind whenever you’re implementing this layer.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now