DataStore and testing

Simona Milanović
Android Developers
4 min readFeb 16, 2022

--

In this final post of our Jetpack DataStore series, we will be covering how to test your DataStore successfully.

Testing DataStore

Every good story needs good testing! To wrap up our series, we will go over how to approach testing your DataStore. Again, we’ll be referring to the Preferences codelab as a starting point. However, keep in mind you can use this material for setting up Proto DataStore testing, as it would be very similar to Preferences.

To test DataStore fully, first you need to go through the setup for instrumentation testing. This would enable us to verify that real updates are being made to our storage, according to our expectations, as we’d be writing and reading from an actual file.

It is also possible to just mock your DataStore instance and inject it as a dependency to another class when unit testing, but you wouldn’t be able to run any real verification checks on the DataStore itself.

Preference DataStore tests

In our codelab, the class that is responsible for reading and writing data to our DataStore is UserPreferencesRepository, so we’d want to verify that this class, along with its functions that interact with DataStore, are working as expected.

We create a UserPreferencesRepositoryTest class:

Now that we have the scaffolding for our test, let’s start with creating our test subject, UserPreferencesRepository, and work our way backwards!

To get an instance of UserPreferencesRepository, we need to pass an instance of DataStore:

We’ll build a quick testDataStore instance that would create a separate test file which we can further use for setting and verifying dummy data:

We use the PreferenceDataStoreFactory to create a Preference DataStore instance and pass:

  • testCoroutineScope — a coroutine scope which our test operations will be performed in
  • produceFile — constructs a test file we will use for writing and reading test data, using the testContext from ApplicationProvider:

Since DataStore is based on Kotlin coroutines, we need to make sure our test has the right coroutine setup in place. To do that, we need to add:

This is a CoroutineDispatcher that performs execution of coroutines which is, by default, immediate. That means any tasks scheduled to be run without delay are immediately executed.

This is a scope which provides detailed control over the execution of coroutines for tests. The Job addition allows us to easily cancel the coroutine as part of regular cleanup after each test.

After setting up, our test class should now look like this:

Our first test will just be verifying the state of our data when the testDataStore is created. We’ll get the snapshot of the data without subscribing to the Flow and do a quick check against our expected result. In our UserPreferencesRepository, this is the first function we’d like to test:

When the repository is first created, it checks if the DataStore is empty and sets a default instance of our data in that case:

If we think about the necessary steps for this test case, we would need to:

  1. Create a testDataStore with default values stored — done ✅
  2. Create the test subject UserPreferencesRepository — done ✅
  3. Set the expectedUserPreferences representing what we’re expecting to find in the testDataStore — not done yet!
  4. Call the repository.fetchInitialPreferences() — not done yet!
  5. Verify the returned values match our expected result — not done yet!

Following those steps, our test should look like this:

💡 You might notice Android Studio complaining there about how “Suspend function ‘fetchInitialPreferences’ should be called only from a coroutine or another suspend function”.

And indeed, our fetchInitialPreferences() is a suspend function, but since we’re calling it from a test, we need to make sure we avoid any real time delays. kotlinx.coroutines.test saves the day yet again with a very simple solution for testing — surround it with testCoroutineScope.runBlockingTest{} (note that, if you’re using Kotlin 1.6, you can replace this with a runTest{})

One down, one to go. Our next test case verifies if our repository is making the right changes to DataStore by enabling or disabling the stored SortOrder value:

We follow a similar pattern:

  1. Create a testDataStore with default values stored — done ✅
  2. Create the test subject UserPreferencesRepository — done ✅
  3. Call the repository.enableSortByDeadline() with a new value — not done yet!
  4. Verify the testDataStore values coming from repository.userPreferencesFlow match our expected result — not done yet!

Adding the last two testing steps would look like this:

That covers our basic test cases. You can follow the same steps to increase the test coverage for this repository class.

Now all that’s left to do is a bit of maintenance and clean up for a healthy testing environment. Finally, let’s take a look at the whole test class now that it’s complete:

And that’s a wrap! 🎬

In our entire series on Jetpack DataStore, we have covered a lot of different topics, all of which are crucial for understanding DataStore in depth and using it correctly in a production environment.

We’ve looked at how DataStore works, what benefits it brings over SharedPreferences and its Preferences and Proto implementations. We also saw which serialization approach to use, how to inject it with Hilt and finally, how to test it. Now it’s up to you to try it out! You can also learn about DataStore from our MAD Skills video series, as well as join our upcoming live Q&A where we’ll answer all questions you might have.

You can find all posts from our Jetpack DataStore series here:
Introduction to Jetpack DataStore
All about Preferences DataStore
All about Proto DataStore
DataStore and dependency injection
DataStore and Kotlin serialization
DataStore and synchronous work
DataStore and data migration
DataStore and testing

--

--

Simona Milanović
Android Developers

Android Developer Relations Engineer @Google, working on Jetpack Compose