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-androidis for mocking on the Android platform.dexopeneris to solve the problem ofopenin Kotlin which we will talk soonkaptAndroidTestis for adding dagger support for UI test, why? Because there is something we need to re-writedaggerMockis 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
finalclass in Java. - But
mockitocan’t mockfinalclass on Android. dexopeneris for solving this problem.- It won’t make your production code
open!
1.2 Some lessons learned in a hard way
- You can
openyour code manually but it’s buggy because sometimesmockitowon’t tell which class hasn’t beenopened 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
.jarfile. You HAVE toopenit by yourself. No way around it.dexopenercan’t mock the 3rd party library. But you can useallopengradle 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
@ComponentA only for testing - This
@Componentwill have a method to get that@Module - During tests, we swap the
@Componentthat the application use with our component A
Then things are easy:
Without
DaggerMock- In the
@Module, instead of return real instance, you just returnmockitomock.
- 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
AppModuleForTestfor more clear and prevent conflicts. Because we already used the nameAppModuleinsrc/main/java/..../AppModule.kt - We moved the
provideABCKey()method fromMainActivityModulehere to theAppModuleForTestto 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 theBuilderinterface 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@ContributesAndroidInjectordecorator 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
@Providesmethods intoAppModuleForTestto make them application wide dependencies. - Add the according activity to
ActivitiesBindingModuleForTestclass.
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
ActivityTestRuleto make it not start the activity, such that we can prepare the mock. - We use
@Mockdecorator to mock thatBooleanKey - In the test, we change the return value of the
nameproperty to “albert” - Then we
launchActivity - We assert whether the
TextViewhas 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.