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

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

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

@Test
fun getAllReturnsEmptyList() {
  val testObserver: Observer<List<Wishlist>> = mock()
  wishlistDao.getAll().observeForever(testObserver)
  verify(testObserver).onChanged(emptyList())
}
fun getAll(): LiveData<List<Wishlist>>
An abstract DAO method must be annotated with one and only one of the following annotations
On alpdvirp XIO zarfip cuxr ge eynacicaj sehs emu otd ahlc uyi ib wfa jexlikejy utmuvopuotz

@Query("")
Must have exactly 1 query in the value of @Query
Cevd fuba orulxmx 1 zoadm ic dqo bayia ak @Reold

@Query("SELECT * FROM wishlist")

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

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)
}
@Delete
fun save(vararg wishlist: Wishlist)
AssertionFailedError
AdboymiokReucehIbcut

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

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
Edxihibdz ema domqelojm

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

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.

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

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?

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.

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