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

9. Testing the Persistence Layer
Written by Tori Gonda

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 Room 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 Room. 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 Room database for the Wishlist app. This app provides a place where you can keep track of the wishlists and the 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. If you are continuing from Chapter 8, “Integration,” notice there are a couple differences between the projects. It is recommended you continue by using the starter project for this chapter. If you choose to continue with your project from Chapter 8, “Integration,” copy and override the files that are different from the starter project for this chapter. The files you’ll need to copy are WishlistDao.kt, KoinModules.kt, RepositoryImpl.kt and WishlistDatabase.kt.

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. However, if you try to save something right now it won’t work! 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 files you should be familiar with before getting started. Open these files and take a look around:

  • WishlistDao.kt: Is the database access object. You will work on defining the database interactions in this class. This is also the class you will write your tests for.

  • RepositoryImpl.kt: This class should be familiar to you from Chapter 8, “Integration.” It’s the repository that hooks your app up with the database.

  • KoinModules.kt: This handles the dependency injection for the app, specifying how to create any dependencies.

  • StringListConverter.kt: This is a helper object to convert lists of Strings to a single String and back again for the purposes of storing in the database.

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 {
}

The AndroidJUnit4 class you’re including here is a cross environment JUnit4 runner for Android tests. Note that this is only required when using a mix of JUnit3 and JUnit4.

Continuing your set up, add the following test rule to your test class:

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

Android Architecture Components uses an asynchronous background executor to do its work. InstantTaskExecutorRule is a rule that swaps out that executor and replaces it with a synchronous one. This will make sure that, when you’re using LiveData, it’s all run synchronously in the tests.

You also need to create the properties to hold your WishlistDatabase and WishlistDao. Add these properties to your test class now:

private lateinit var wishlistDatabase: WishlistDatabase
private lateinit var wishlistDao: WishlistDao

The WishlistDao is what you are performing your tests on. To create an instance of this class, you’ll need an instance of the WishlistDatabase first. You will initialize these in a @Before block in a moment.

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.

Tests must be repeatable. This means you should be able to run a test multiple times with the same result. There’s also a requirement that one test cannot influence the outcome of another. What if you’re testing for an empty database but there are items left over from another test? You will need to clear data between tests.

You can solve this problem by using an in-memory database. Room luckily provides a way to easily create one. Add this to your test class, importing androidx.test.platform.app.InstrumentationRegistry:

@Before
fun initDb() {
  // 1
  wishlistDatabase = Room.inMemoryDatabaseBuilder(
      InstrumentationRegistry.getInstrumentation().context,
      WishlistDatabase::class.java).build()
  // 2
  wishlistDao = wishlistDatabase.wishlistDao()
}
  1. Here you’re using a Room builder to create an in-memory WishlistDatabase. Compare this to the database creation in KoinModules.kt. Information stored in an in-memory database disappears when the tests finish, solving your state issue.
  2. You then use this database to get your WishlistDao.

Almost done setting up! After your tests finish, you also need to close your database. Add this to your test class:

@After
fun closeDb() {
  wishlistDatabase.close()
}

Now you’re ready to start writing tests.

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 com.nhaarman.mockitokotlin2.* for mock() and verify():

@Test
fun getAllReturnsEmptyList() {
  val testObserver: Observer<List<Wishlist>> = mock()
  wishlistDao.getAll().observeForever(testObserver)
  verify(testObserver).onChanged(emptyList())
}

This tests the result of a LiveData response similar to how you wrote your tests in Chapter 7, “Introduction to Mockito.” You create a mock Observer, observe the LiveData returned from getAll() with it, and verify the result is an empty list.

You have one error right now, that getAll() is unresolved. Add the following to WishlistDao:

fun getAll(): LiveData<List<Wishlist>>

Everything looks pretty good, so try to run the test. Oh no! There’s still a compiler error.

An abstract DAO method must be annotated with one and only one of the following annotations
An abstract DAO method must be annotated with one and only one of the following annotations

Hmmm. The next minimum thing to make this test run is to add a @Query annotation. Add an empty query annotation to getAll():

@Query("")

Try running it with this change. If you are familiar with Room, you may know what’s coming.

Must have exactly 1 query in the value of @Query
Must have exactly 1 query in the value of @Query

The compiler enforces that you include a query in the parameter. That means the next step is to fill in the query as simply as possible. Fill in your query with the following:

@Query("SELECT * FROM wishlist")

Run your test and it finally compiles! But… it’s passing. When practicing TDD you always want to see your tests fail first. You never saw a state where the test was compiling and failing. You were so careful to only add the smallest bits until it compiled. Room made it really hard to write something that didn’t work. Maybe the real question is “Should you be testing this?” The answer to this question is important.

Knowing not to test the library

The fact that Room 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. :]

Sometimes this is a gray line to try to find, and it’s one of the things that makes testing persistence difficult. It’s a part of your code that likely relies heavily on a framework.

It’s a balance to test that your interactions with the framework or library are correct without testing the library itself. Watch out for cases like these and use your best judgement. Over time you’ll gain some kind of intuition on which tests are valuable, and what tests are better left to the library’s contributors.

In this case it’s not up to you to test the Room framework. It’s those writing Room’s responsibility to make sure that when the database is empty, it returns nothing. Instead you want to test that your logic, your queries, and the code that depends on them are working correctly.

With that in mind, you can move on to test other database interactions.

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, keeping in mind that save() is not yet resolved:

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

Here you:

  1. Create a couple wishlists and save them to the database. At this point save() does not exist yet, so there will be an error.
  2. Use your mock testObserver again to call getAll().
  3. Create an ArgumentCaptor to capture the value in onChanged(). Using an ArgumentCaptor from Mockito allows you to make more complex assertions on a value than equals().
  4. Test that the result from the database is a non empty list. At this point you care that data was saved and not what was saved, so you’re checking the list size only.

Great! Next, to make it compile and run, you need to add a save() function to the WishlistDao:

@Delete
fun save(vararg wishlist: Wishlist)

You need to have a database interaction annotation in order for this to compile, as you learned earlier in this chapter. You also want to see this test failing, so you’re using the wrong one, @Delete. Run your test and see it fail.

AssertionFailedError
AssertionFailedError

You know the drill, time to make this test green!

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)

Using OnConflictStrategy.REPLACE allows the the database to override an entry that already exists.

Run your tests, and they should all be green.

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

This is almost the same as your previous test with the exception of the final line. In that line you’re testing that the list result contains the exact wishlists you expect.

Build and run your tests. It may come as a surprise, but they failed! Why is that? Insert a debugger breakpoint on the assertion line and inspect the capturedArgument at that point when you run it again, using the debugger. Huh! Somehow there is a list with an empty string in it.

Good investigating! You found the bug before it reached production. Time to solve the problem.

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. Replace the body of stringToStringList() with:

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

Now run those tests again and see them pass!

Note: All over while testing you’ll be performing verifications that rely heavily on the equals() method to perform comparisons, containsAll() being one of them. Much of the time in these cases you are looking for data equality (the properties of both objects are exactly the same) rather than object equality (they reference the same object in memory). Because of this, you want to make sure your equals() performs the way you expect. In Kotlin, this is often as simple as making your class a data class. This overrides equals() and hashcode() for you to compare the properties. Take caution for the times where this isn’t enough! For example, Kotlin may not compare Lists the way you expect. For that reason equals() and hashcode() are overridden for Wishlist in this app. You can see this in Wishlist.kt.

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

Here you:

  1. Create and save some wishlists, same as your other tests.
  2. Query for a specific wishlist, wishlist2, and verify the result is correct.

Now to write the minimum code to make it compile. Add this to the WishlistDao:

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

Notice it’s intentionally incorrect. It’s searching for a wishlist where the id is not the given id. This is again to make sure you see a failing test.

Run that test and verify it really does fail.

Arguments are different
Arguments are different

Making the test pass

It’s the last time you’ll do it 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")

Ready? Run your tests to see them all pass.

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 set up time as you write other tests. This tool is called test data creation.

If you look at the tests you’ve written in this chapter, as well as Chapter 8, “Integration,” you see many lines where you’re manually creating a Wishlist. Not only is this tedious, but you’re only testing that your code works for that specific Wishlist.

One way to abstract this work and make your data random, is by using a Factory. A Factory object will create instances of your data class with random values for the properties. This will make your tests stronger and easier to write!

Start by creating a WishlistFactory object in your test directory. As this is specific to your persistence right now, a good place to put it is app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ wishlist ‣ persistence ‣ WishlistFactory.kt:

object WishlistFactory {
}

If you use this in other places as well, you can move this Factory to a more convenient location. Since you’re only using it in this one test right now, this location works great.

You need a way to create random values for your data class properties. A simple way to do this is to create helper methods to create them, one for each type of property you need. Again, you could place these in a reusable location, but because you’re only using them here right now, they can share the WishlistFactory.

In your Wishlist you need two types of data: String and Int. Add these methods to your WishlistFactory:

// 1
private fun makeRandomString() = UUID.randomUUID().toString()
// 2
private fun makeRandomInt() =
    ThreadLocalRandom.current().nextInt(0, 1000 + 1)

These are simple, built in ways to create random values. You can use similar ways to create helpers for Long, Boolean, etc.

Now, to finish the Factory, add a method to create a Wishlist:

fun makeWishlist(): Wishlist {
  return Wishlist(
      makeRandomString(),
      listOf(makeRandomString(), makeRandomString()),
      makeRandomInt())
}

You use the random value methods you just created to set the properties, knowing you will likely have a completely different Wishlist every time you create one. They won’t look anything like what’s on your wishlist, but they will be unique. Well, the Wishlist won’t have what you want unless you want a UUID for your birthday.

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

So clean! Run your tests to make sure they still pass.

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 RepositoryImpl and change the body of the functions to match the following:

override fun saveWishlist(wishlist: Wishlist) {
  wishlistDao.save(wishlist)
}

override fun getWishlists(): LiveData<List<Wishlist>> {
  return wishlistDao.getAll()
}

override fun getWishlist(id: Int): LiveData<Wishlist> {
  return wishlistDao.findById(id)
}

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

All this is doing is hooking up the repository to call the newly implemented methods on the WishlistDao. You’re ready to build and run the app!

Note: If the RepositoryImpl looks familiar to you, that’s because you saw it in Chapter 8, “Integration.” The implementation was removed for the start of the chapter as the WishlistDao interface was emptied so you could test it.

Play around with your fully functioning app! Create some Wishlists and add some items to them. You’ll always know the perfect gift to give now.

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 Room for your persistence?

Unfortunately, many libraries don’t have these convenient, built-in testing helpers. Some do, such as Realm, but often you’re left in the dust. In these cases you’re usually left to clear the persisted data before each test. When doing this, make sure your testing device doesn’t have any data you want to keep for that app!

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.
  • If the persistence library you’re using doesn’t have built in strategies for testing, you may need to delete all persisted data before 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.

As with all of programming, there are often many ways to do the same thing. For another example of how to test RoomDB, take a look at “Room DB: Advanced Data Persistence” https://www.raywenderlich.com/5686-room-db-advanced-data-persistence. You may even start to think how you can use the strategy in this tutorial for the tests you wrote in Chapter 8, “Integration.” ;]

For an example of how to test an SQLite database, take a look at “Testing persistence in the Android ecosystem” https://blog.novoda.com/testing-persistence-in-the-android-ecosystem/.

Moving on to an equally important layer, in the next chapter, Chapter 10, “Testing the Network Layer,” you’ll learn strategies for testing code that relies on API network calls.

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.