Android Native - provide fake DataSources to Repositories in Unit Tests

dimitrilc 3 Tallied Votes 634 Views Share

Introduction

In Android projects, DataSource classes act as entry points to interacting with local or remote data sources. Their dependencies tend to be HTTP clients, the database, or DAOs, and their dependents are usually Repository classes.

In this tutorial, we will learn how to provide a fake DataSource to a Repository in unit tests.

Note that DataSource classes mentioned in this tutorial refers to classes following the naming convention of type of data + type of source + DataSource and not some specific framework class.

Goals

At the end of the tutorial, you would have learned:

  1. How to create fake DataSource classes.

Tools Required

  1. Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1 Patch 1.

Prerequisite Knowledge

  1. Basic Android.
  2. Basic Unit Testing.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Create a class called UserLocalDataSource using the code below. This is just a simple class that with a few functions to keep it simple. It also contains two dependencies: a database and a web service.

     class UserLocalDataSource(
        private val dataBase: Any,
        private val httpClient: Any
     ) {
        fun getUserName() = "User Name"
        fun getUserBirthday() = "User Birthday"
        fun getUserAddress() = "User Address"
     }
  3. Create a class called UserRepository using the code below. This class depends on all functions of UserLocalDatasource.

     class UserRepository(private val userLocalDataSource: UserLocalDataSource) {
        fun getUserData() = userLocalDataSource.getUserName()
        fun getUserBirthday() = userLocalDataSource.getUserBirthday()
        fun getUserAddress() = userLocalDataSource.getUserAddress()
     }
  4. Create the class UserRepositoryUnitTest in the test source set using the code below. Ignore the compile error for now.

     class UserRepositoryUnitTests {
    
        private val repo = UserRepository(UserLocalDataSource())
    
        @Test
        fun getUserData_isCorrect(){
            repo.getUserData()
        }
    
        @Test
        fun getUserBirthday_isCorrect(){
            repo.getUserBirthday()
        }
    
        @Test
        fun getUserAddress_isCorrect(){
            repo.getUserAddress()
        }
     }

The Problems with NOT using a Fake

At this point, there is a problem that exist in our application.

Because UserRepository depends directly on the UserLocalDatasource class, we are forced to instantiate a real instance of UserLocalDataSource in our unit tests as well. This makes it hard to set up the tests, especially when UserLocalDataSource is also dependent on other classes; this means that we will have to also set up dependencies for UserLocalDataSource in the tests (and probably dependencies of those dependencies).

Unit tests are supposed to focus only on the class being tested, and they should execute quickly.

Replacing concrete dependencies with Interfaces

A good method to fix the problem listed in the previous section would be to replace the concrete dependency of UserLocalDataSource in UserRepository with an interface instead. This makes it very easy to switch out the real implementation with a fake implementation in unit tests.

Follow the steps below to make UserRepository depend on an abstract interface:

  1. Open UserLocalDataSource.

  2. Right-click on the class name -> Refactor -> Rename.

  3. Change it to UserLocalDataSourceImpl.

     class UserLocalDataSourceImpl(
        private val dataBase: Any,
        private val httpClient: Any
     ) {
        fun getUserName() = "User Name"
        fun getUserBirthday() = "User Birthday"
        fun getUserAddress() = "User Address"
     }
  4. Now, create the interface UserLocalDataSource using the code below.

     interface UserLocalDataSource {
        fun getUserName(): String
        fun getUserBirthday(): String
        fun getUserAddress(): String
     }
  5. Back to the UserLocalDataSourceImpl class, make it implements UserLocalDataSource. You will have to prefix all the functions in UserLocalDataSourceImpl with the keyword override.

     class UserLocalDataSourceImpl(
        private val dataBase: Any,
        private val httpClient: Any
     ): UserLocalDataSource {
        override fun getUserName() = "User Name"
        override fun getUserBirthday() = "User Birthday"
        override fun getUserAddress() = "User Address"
     }
  6. Now that we have the interface UserLocalDataSource, we can use that as a dependency for UserRepository instead of the concrete type UserLocalDataSourceImpl. In the UserRepository, replace UserLocalDataSourceImpl with UserLocalDataSource**.

     class UserRepository(private val userLocalDataSource: UserLocalDataSource) {
        fun getUserData() = userLocalDataSource.getUserName()
        fun getUserBirthday() = userLocalDataSource.getUserBirthday()
        fun getUserAddress() = userLocalDataSource.getUserAddress()
     }

Creating a fake DataSource

Because UserRepository can now receive any instance of UserLocalDataSource, including fake ones, we no longer have to worry about providing a real implementation of UserLocalDataSourceImpl and its dependencies (database and web service).

Create a fake UserLocalDataSource called FakeUserLocalDataSource (in the test source set) using the code below.

class FakeUserLocalDataSource: UserLocalDataSource {
   override fun getUserName() = "User Name"
   override fun getUserBirthday() = "Birthday"
   override fun getUserAddress() = "Address"
}

The only important thing that you need to be aware of when overriding UserLocalDataSource in a fake is that you return the correct data that needs to be tested. If you are familiar with testing, the class above can also be called a Stub because it simply returns hard-coded data.

Finally, in the UserRepositoryUnitTests class, you can just provide UserRepository with an instance of FakeUserLocalDataSource without having to provide it any other dependencies.

private val repo = UserRepository(FakeUserLocalDataSource())

Solution Code

UserLocalDataSource.kt

interface UserLocalDataSource {
   fun getUserName(): String
   fun getUserBirthday(): String
   fun getUserAddress(): String
}

UserLocalDataSourceImpl.kt

class UserLocalDataSourceImpl(
   private val dataBase: Any,
   private val httpClient: Any
): UserLocalDataSource {
   override fun getUserName() = "User Name"
   override fun getUserBirthday() = "User Birthday"
   override fun getUserAddress() = "User Address"
}

UserRepository.kt

class UserRepository(private val userLocalDataSource: UserLocalDataSource) {
   fun getUserData() = userLocalDataSource.getUserName()
   fun getUserBirthday() = userLocalDataSource.getUserBirthday()
   fun getUserAddress() = userLocalDataSource.getUserAddress()
}

FakeUserLocalDataSource.kt

class FakeUserLocalDataSource: UserLocalDataSource {
   override fun getUserName() = "User Name"
   override fun getUserBirthday() = "Birthday"
   override fun getUserAddress() = "Address"
}

UserRepositoryUnitTests.kt

class UserRepositoryUnitTests {

   private val repo = UserRepository(FakeUserLocalDataSource())

   @Test
   fun getUserData_isCorrect(){
       repo.getUserData()
   }

   @Test
   fun getUserBirthday_isCorrect(){
       repo.getUserBirthday()
   }

   @Test
   fun getUserAddress_isCorrect(){
       repo.getUserAddress()
   }
}

Summary

We have learned how to create a fake DataSource in this tutorial. Creating fakes is not limited to only Android or DataSource classes. You can also create fakes for Repository classes, web services, etc. The full project can be found at https://github.com/dmitrilc/DaniwebAndroidFakeDataSourceUnitTest.

Vu_812 commented: thanks +0
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.