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:
- How to create fake DataSource classes.
Tools Required
- Android Studio. The version used in this tutorial is Android Studio Chipmunk 2021.2.1 Patch 1.
Prerequisite Knowledge
- Basic Android.
- Basic Unit Testing.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
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" }
-
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() }
-
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:
-
Open UserLocalDataSource.
-
Right-click on the class name -> Refactor -> Rename.
-
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" }
-
Now, create the interface UserLocalDataSource using the code below.
interface UserLocalDataSource { fun getUserName(): String fun getUserBirthday(): String fun getUserAddress(): String }
-
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" }
-
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.