Android Kotlin: Leveraging Mockito and Dagger 2 for Mocking and Dependency Injection in Integration/UI Tests
In this article, I will guide you through the process of setting up Dagger 2 for dependency injection and demonstrate how to mock dependencies using Mockito in your Android integration/UI tests. It is assumed that you already possess a certain level of familiarity with Dagger 2 and Mockito.
Let’s begin by installing the required dependencies in the Gradle files. Open the Gradle file of your app module and include the following dependencies:
implementation “com.google.dagger:dagger:$daggerVersion”
kapt “com.google.dagger:dagger-compiler:$daggerVersion”
kapt “com.google.dagger:dagger-android-processor:$daggerVersion”
kaptTest “com.google.dagger:dagger-compiler:$daggerVersion”
androidTestUtil ‘androidx.test:orchestrator:1.2.0’
testImplementation ‘junit:junit:4.12’
testImplementation ‘androidx.test:core:1.2.0’
testImplementation ‘org.mockito:mockito-core:2.7.22’
androidTestImplementation ‘junit:junit:4.12’
androidTestImplementation “androidx.test.espresso:espresso-core:$expressoVersion”
androidTestImplementation “androidx.test.espresso:espresso-intents:$expressoVersion”
androidTestImplementation ‘androidx.test:runner:1.2.0’
androidTestImplementation ‘androidx.test:rules:1.2.0’
androidTestImplementation ‘androidx.test.uiautomator:uiautomator:2.2.0’
androidTestImplementation ‘com.android.support.test:runner:1.1.1’
androidTestImplementation ‘org.mockito:mockito-android:2.7.22’
In the provided sample code snippet, certain dependencies such as Espresso and UI Automator may not be essential for every project. They are included here because they are utilized in the author’s specific project. Please examine the dependencies and eliminate any that are not necessary for your particular project setup.
Additionally, remember to adjust the testInstrumentationRunner in your app module’s build.gradle
file to utilize the custom class. Here’s how you can accomplish this:
android { compileSdkVersion 29 buildToolsVersion “29.0.2” defaultConfig { applicationId “your.package.name” minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName “1.0” // testInstrumentationRunner “androidx.test.runner.AndroidJUnitRunner” testInstrumentationRunner “your.package.name.MockTestRunner”}//the rest of the code}
We are creating a custom MockTestRunner class because it allows us to inject our own custom application class for tests, which differs from the actual application class.
Here is the implementation of the MockTestRunner
:
import android.app.Applicationimport android.content.Contextimport androidx.test.runner.AndroidJUnitRunnerimport your.package.name.MockApplicationControllerclass MockTestRunner: AndroidJUnitRunner(){ override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, MockApplicationController::class.java!!.getName(), context) }}
As observed, we are injecting our custom MockApplicationController
application class, specially designed for testing purposes.
Firstly, create a class named ApplicationController
extending from the Application class. Subsequently, create another class named MockApplicationController
extending from the previously created ApplicationController
class. I will delve into the implementation of these classes later.
First, I will guide you through the configuration/setup of Dagger 2 for dependency injection.
Configuring Dagger 2 for Dependency Injection
Initially, create a class named AppModule
with the following code.
import android.app.Application
import android.content.Context
import your.package.name.services.*
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
open class AppModule (private val app: Application) {
@Singleton
@Provides
fun provideContext(): Context = app
@Singleton
@Provides
open fun userService(): IUserService {
return ConcreteUserService()
}
}
In the provided code, the IUserService
interface serves as the interface for your service class. Two classes will extend this interface: one is the concrete class containing the actual implementation, and the other is the mock version utilized in tests.
Next, you’ll need to create a Dagger app component interface. Create an interface named AppComponent
with the following code.
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [ AppModule::class ])
interface AppComponent
{
fun inject(app: ApplicationController)
fun inject(app: MockApplicationController)
}
As evident in the code, there are two methods with the same name but different arguments. These methods are employed for injecting dependencies. The mock classes will be injected into the MockApplicationController
class, while the concrete classes are injected into the ApplicationController
class.
Next, you’ll also need to create an app module class for the tests. Create a class named TestAppModule
with the following code.
import android.app.Application
import android.content.Context
import your.package.name.services.*
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
open class TestAppModule (private val app: Application) {
@Singleton
@Provides
fun provideContext(): Context = app
@Singleton
@Provides
open fun userService(): IUserService {
return MockAuthService()
}
}
As observed, the code closely resembles the implementation of the AppModule
class. The only distinction is that the userService()
method now returns an instance of the MockAuthService
class, as this class will be utilized for the tests.
You also need to create an interface named TestAppComponent
with the following code, which will be utilized in the tests.
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [ TestAppModule::class ])
interface TestAppComponent: AppComponent
{
}
Now, all the necessary classes for configuring Dagger have been created.
Step 2:
The next step involves implementing dependency injection. Instead of injecting dependencies (such as service classes or repositories) directly into individual activity or fragment classes, we will inject them into our custom Application classes (ApplicationController
and MockApplicationController
). Subsequently, we can use these dependencies throughout the entire application by accessing them through the custom application classes. I trust that you grasp the concept to some extent.
Initially, we need to provide the implementation for the previously created ApplicationController
class with the following code.
open class ApplicationController: Application()
{
lateinit var appComponent: AppComponent
protected open fun initDagger(app: ApplicationController): AppComponent {
return DaggerAppComponent
.builder()
.appModule(AppModule(app)).build()
}
@Inject
lateinit var userService: IUserService
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}}
Let me elucidate the functionality of the ApplicationController
class. In this class, we have a method named initDagger
responsible for initializing the Dagger app component. This method is then invoked within the onCreate
event of the ApplicationController
class. Following the initialization, we proceed to inject the dependency—in our case, the IUserService
. If you revisit the AppComponent
class, you'll notice two inject
methods: one for the ApplicationController
and the other for the MockApplicationController
. In this instance, we are passing this
to the inject
method, signifying the instance of the ApplicationController
class. Consequently, we inject the dependency into the ApplicationController
class.
For the ApplicationController
class, the AppModule
class is utilized. Examining the definition of the userService
method in AppModule
, you'll find it returns an instance of the ConcreteUserService
class. This instance is then injected into the userService
property of the ApplicationController
class. Now, we have successfully configured Dagger for the actual application. In the subsequent step, we will configure Dagger 2 for dependency injection in tests.
We need to furnish the implementation for the MockApplicationController
class with the following code.
class MockApplicationController: ApplicationController()
{
override fun initDagger(app: ApplicationController): AppComponent {
return DaggerTestAppComponent
.builder()
.testAppModule(TestAppModule(app))
.build()
}
}
Essentially, what we are doing is overriding the initDagger
method to use classes specifically designed for tests. When the method to initialize Dagger is invoked with this
as the parameter, it passes the instance of the MockApplicationController
class. Consequently, it employs the inject
method that has MockApplicationController
as a parameter. Additionally, it utilizes TestAppModule
over AppModule
. As evident in the TestAppModule
class, the userService
method returns the MockAuthService
instance. The concept here is to provide the mock implementation for tests in this class.
How does Dagger 2 determine the correct userService
method to invoke and inject the appropriate object?
As evident in the ApplicationController
class, we annotated the userService
property with the @Inject
annotation, indicating that the dependency will be injected into that field. Both the ConcreteUserService
class and the MockUserService
class implement the IUserService
interface. Therefore, we specify the type as the IUserService
interface. Dagger calls the methods in the AppModule
class and the TestAppModule
class, aligning the return type of the method with the field type.
For instance, in the ApplicationController
class, it utilizes the AppModule
class. Since the userService
property is declared with the type IUserService
, Dagger looks for the method that returns a type compatible with the IUserService
interface. In the case of the AppModule
class, it is the userService
method, which returns an instance of the ConcreteUserService
class. For TestAppModule
, it would be the method returning an instance of the MockUserService
class.
Up to this point, we have successfully configured and set up Dagger 2.
Now, for your tests, you can implement the mock logic in the MockUserService
class.
You will utilize the instance of the userService
field from the ApplicationController
class within your activity or fragment to make API calls or perform actions as needed. If you declare the activity rule within your test class as shown below and launch the activity, the MockUserService
class will be injected, thereby overriding the behavior of the ConcreteUserService
class.
@get:Rule
var mainActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>(MainActivity::class.java, true, false)
You can launch the activity in your test using the following approach:
this.mainActivityRule.launchActivity(intent)
What if we aim to inject Mockito mock objects instead?
At times, we may wish to substitute the Mock classes with Mockito mock objects. To achieve this, we need to make slight adjustments to the TestAppModule
class.
The updated version of the TestAppModule
class is as follows:
@Module
open class TestAppModule (private val app: Application) { private lateinit var userService: IUserService fun setUserService(userService: IUserService) {
this.userService = userService } @Singleton
@Provides
fun provideContext(): Context = app @Singleton
@Provides
open fun userService(): IUserService { if (::userService.isInitialized) return this.userService return MockAuthService()
}
}
Essentially, what we’ve done is provide a mechanism to set a custom mock object. Additionally, you’ll need to include some extra code to utilize the Mockito mock object.
To use Mockito, the initial step is to declare a property within the test class adorned with the @Mock
annotation.
@Mock
private lateinit var userServiceMock: IUserService
Next, we need to override the logic for initializing the Dagger app component for the test. Typically, the @Before
method would be a suitable place to include this logic. The following code illustrates this.
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val instrumentation = InstrumentationRegistry.getInstrumentation()
val app = instrumentation.targetContext.applicationContext as MockApplicationController
var testModule = TestAppModule(app)
testModule.setUserService(this.userServiceMock)
app.appComponent = DaggerTestAppComponent
.builder()
.testAppModule(testModule)
.build()
app.appComponent.inject(app)
}
As observed, we are now injecting using the Mockito mock object placed into the TestAppModule
object. Consequently, we can apply any assertions or methods provided by Mockito to the object within the test.
That concludes the article. I hope you find it helpful.
If you want to reach out to me, https://www.linkedin.com/in/wai-yan-hein-b99162123/.