Unit Testing With Dagger 2: Brewing a Potion With Fakes, Mocks, and Custom Rules in Android
If you have Dagger in your project but never used it for testing, it’s high time to give it a try. Dagger makes your code so much easier to test. Once the test setup is complete, all you have to do is plug and play with the dependencies. Also, dependency injection is a crucial concept in testing, and you should use it whenever possible.
I highly recommend having hands-on experience in Dagger and Unit testing before going through the article. It’s required to have the fundamental knowledge of Dagger to understand how things work under the hood.
Exploring the Test Application
Overview
The sample app has only one screen that searches through Github repo by a keyword. The app fetches the API response through a Data Repository and outputs the result inside a ViewModel
. Finally, an Activity
watches for LiveData
changes and sets the output inside a TextView
.
Dagger Configuration
The test project has five application-level modules and one module for each Activity
.
The functionality of each module:
ActivityBuildersModule
This module works as an entry point for all of the Activities. Whenever there’s a new Activity
in the app, we can add it to the module with a @ContributesAndroidInjector
annotation. Inside the annotation, we can pass the modules for the given Activity
, e.g., Fragment
, ViewModel
. It will create a sub-component for the modules and add it as a child to the Activity
.
AppModule
It returns app-specific dependencies created with a @Provides
annotation.
AppModuleBinds
It returns app-specific dependencies created with a @Binds
annotation.
NetworkModule
It returns everything related to the Networking stuff. E.g., Retrofit
, OkHttp
etc.
AppComponent
AppComponent
hosts all modules and facilitates the Dagger dependency tree.
BaseApplication
Finally, We have our BaseApplication
class to initialize the Dagger process.
Test Goals
For the sake of simplicity, let’s test the DefaultDataRepository
from the data package. It has only one method, getSearchResponse()
to search through Github for repositories by a keyword. Let’s create our test class and try to prepare an instance of the DefaultDataRepository
. Note that,DefaultDataRepository
constructor requires a DataSource
as an argument. First, we will check the traditional approach of providing dependency and switch to a Dagger dependency later.
Here, we have a mock of the DataSource
. Whenever we call the getSearchResponse()
from DataSource
, the mock will return a fake response. Our test needs to verify if the dataRepository.getSearchResponse()
actually trigged the remoteDataSource.getSearchResponse()
function with proper arguments.
Notes:
- The test function is wrapped inside a
runBlockingTest
. It’s part of thekotlinx.coroutines.test
library and helps running any suspend function inside a test. - Since we are using MockK,
coEvery
andcoVerify
are specially designed to handle any suspend function.
Run the test, and it should succeed. So far, everything looks good.
Blending Dagger for the Unit Test
We already created a successful Unit test by using mocks. Our effort to create Dagger dependencies will never pay off if we can’t use them in the Unit test. Our goal is to access the following classes in Unit tests and use them as dependencies.
- Modules (To provide all of our dependencies)
- AppComponent (To host all modules and create the dependency tree)
- BaseApplication (To trigger the Dagger injection)
We will take one step at a time to make the solution.
TestAppModule
We are only interested in AppModule
. Since we have to change the behavior of the RemoteDataSource()
function only. Let’s create a TestAppModule
that provides the same type of DataSource()
instance but a mock one.
TestAppComponent
We can use the same AppComponent
from our app, but we might need to exclude or create a mock version of the Unit test modules. It would be wise not to mess up with the real AppComponent
. We will make a TestAppComponent
that extends from AppComponent
but has some Unit test specific changes. Also, we need to create a function for passing an instance of the Test class. Dagger needs this to enable field injection for the Test class.
TestBaseApplication
By default, BaseApplication
will launch whenever we run our app on the device. Let’s not mess up with our real BaseApplication
class and create a test class that extends it. This way, we will have a separate but the same type of Application
class for the app.
But how can we load up the TestBaseApplication
in our test?
Introducing the Test Runner
We can use a Unit test runner to load our Application class whenever the Unit test runs. We will create a new Rule class and extend it from the AndroidJUnitRunner
.
Once the Test Runner is ready, let’s plug it into the build.gradle
file.
Wrapping up the test class
We are close to the Dagger setup. Let’s prepare the Dagger injection inside the @Before
method so that before each test, a new configuration will be ready. This way, changes in dependencies from one test won’t affect the logic in other Unit tests.
Now, we can update the test dependencies with @Inject
annotation and re-run our test.
Abracadabra, We spelled our first magic with the potion.
Source Code
The source code for the article is available on Github.