How to mock dagger-android injection in instrumented tests with Kotlin
Previously, we talked about how to do mock using dagger 2 and dagger-android. But as the wise man said: If You Didn't Test It, It Doesn't Work
. So let’s see how to do the test. An important part of doing the UI test is mock, we don’t really want to deal with network request even it allows us to. Today, I share the knowledge of how to mock the injections from dagger-android
in the UI test (instrumented tests). I write this because most of the online tutorials are using dagger-android
in a dagger 2
way which leads to more code, or even worse, some mixed up usage will make people even more confused. Even though confuse
is a word that tends to be bound with dagger
. :D Oh, well, it’s a bad joke.
1. Set up
Add something to your build.gradle
of the app
module (We start with the project created from last dagger-android
blog which is a default project created by Android Studio):
1 | apply plugin: 'kotlin-kapt' |
I will only show the lines we need to add.
mockito-android
is for mocking on the Android platform.dexopener
is to solve the problem ofopen
in Kotlin which we will talk soonkaptAndroidTest
is for adding dagger support for UI test, why? Because there is something we need to re-writedaggerMock
is for making the whole procedure easy by linking mockito mocked object to your tests
1.1 What problem does which Kotlin default compiles to
solve
- Kotlin default compiles your class to
final
class in Java. - But
mockito
can’t mockfinal
class on Android. dexopener
is for solving this problem.- It won’t make your production code
open
!
1.2 Some lessons learned in a hard way
- You can
open
your code manually but it’s buggy because sometimesmockito
won’t tell which class hasn’t beenopen
ed and will give you some error message which is totally irrelevant. But withdexopener
, it will mark all things asopen
. - If you want to mock methods from a
.jar
file. You HAVE toopen
it by yourself. No way around it.dexopener
can’t mock the 3rd party library. But you can useallopen
gradle plugin to make your life a little bit better.
2. Basic Idea
The basic idea here is:
- You still generate instances for injection in
@Module
- But We’ll create new
@Component
A only for testing - This
@Component
will have a method to get that@Module
- During tests, we swap the
@Component
that the application use with our component A
Then things are easy:
Without
DaggerMock
- In the
@Module
, instead of return real instance, you just returnmockito
mock.
- In the
With
DaggerMock
- You declare the type you want to swap and mock it
- You can then use the mock.
- No need to change the
@Module
3. Now let’s create the Dagger Modules and Components only for testing
First, create a folder named debug
at the same level of main
. Then put a java
folder in it, and we will start. Will create several files which only used in testing.
3.1 Add @Module
only for testing
1 |
|
Something is different from our previous blog:
- We changed the name to
AppModuleForTest
for more clear and prevent conflicts. Because we already used the nameAppModule
insrc/main/java/..../AppModule.kt
- We moved the
provideABCKey()
method fromMainActivityModule
here to theAppModuleForTest
to make it application wide. Because it makes mock easier.
3.2 Add @Component
only for testing
1 |
|
This is nearly identical to our original version of AppComponent
, just one change:
- We add an
appModuleForTest()
in theBuilder
interface for swapping modules later when testing.
3.3 Add Activity binding @Module
1 |
|
This is nearly identical to our original version of AppComponent
, just one change:
- We removed the
(modules = [MainActivityModule::class])
from the@ContributesAndroidInjector
decorator because all dependencies are now inAppModuleForTest
. They all become application wide dependencies.
3.4 For easy usage in the future
Sounds like a lot, but in fact, it’s very easy. Every time you want to add new dependencies for testing:
- Copy all
@Provides
methods intoAppModuleForTest
to make them application wide dependencies. - Add the according activity to
ActivitiesBindingModuleForTest
class.
That’s it.
4. For those who don’t want to use DaggerMock user
Now everything is set up, in the AppModuleForTest.kt
, instead of returning the real implementation, return the mockito
mocked object.
Why I don’t use this way:
It works, but requires lots of code, thinking of this. How could you prepare for another test suites when you want to change the setup value of mocks. You create new AppModuleForTest
class then swap again?!
Come on, there must be some better solutions to this.
Knowing we can do things like this, is just for better understanding what DaggerMock
does for us underneath.
5. For DaggerMock user
First we create a new file named espressoDaggerMockRule.kt
in the androidTest/java/your-package-path/
:
1 | fun espressoDaggerMockRule() = DaggerMock.rule<AppComponentForTest>(AppModuleForTest()) { |
What it does is just swap the @Module
with our AppModuleForTest
class. Then we can build the dependency graph. And every time, you mock something in your test, DaggerMock
will look through this graph, find the @Provides
method and change the return value for you, so espresso
will get your mocked version instance instead.
6. Write test
This is our activity:
1 | class MainActivity : DaggerAppCompatActivity() { |
You will see the TextView
sets its text
to the abcKey
which is an instance of BooleanKey
and will be injected by dagger-android
. If you run the app, you will see the value is abc
.
Which @Provides
by a method in MainActivityModule
, but during the test, we will swap with our mocked version. Give it a new value then assert that.
The code for the test is:
1 |
|
What happens here?
- We declared a new rule which uses our
espressoDaggerMockRule
- We give 2 extra parameters to
ActivityTestRule
to make it not start the activity, such that we can prepare the mock. - We use
@Mock
decorator to mock thatBooleanKey
- In the test, we change the return value of the
name
property to “albert” - Then we
launchActivity
- We assert whether the
TextView
has the “albert” or not.
And wow, the test passes!
Elegant, easy and concise code.
7. Talk is cheap, show me the code
8. End
Hope it helps.
Thanks for reading!
Follow me (albertgao) on twitter, if you want to hear more about my interesting ideas.