I've been doing Android instrumentation testing with Dagger, Espresso and Mockito, and I love it. To commemorate the launch of Dagger 2 out of SNAPSHOT, I am sharing a demo repo with Dagger 2, Espresso 2 and Mockito:
https://github.com/chiuki/android-test-demo
Dagger Components
Dependency injection allows our app and test obtain different modules. This is very useful for creating repeatable test cases. The demo app displays today's date in the format yyyy-MM-dd
. We would like to test that against a known date, instead of depend on the actual date when we run the test.
In Dagger 2, a Component
provides modules for your whole app, and defines where to inject them.
public interface DemoComponent { void inject(MainActivity mainActivity); }
@Singleton @Component(modules = ClockModule.class) public interface ApplicationComponent extends DemoComponent { }
@Singleton @Component(modules = MockClockModule.class) public interface TestComponent extends DemoComponent { void inject(MainActivityTest mainActivityTest); }
ApplicationComponent
is used when the app is run normally, and TestComponent
is used during tests. Both components injects into MainActivity
.
How does the MainActivity
know which component to use? It injects via the application, which stores the component.
Approach 1: setComponent
private DemoComponent component = null; @Override public void onCreate() { super.onCreate(); if (component == null) { component = DaggerDemoApplication_ApplicationComponent .builder() .clockModule(new ClockModule()) .build(); } } public void setComponent(DemoComponent component) { this.component = component; } public DemoComponent component() { return component; }
We call setComponent()
in test, which runs before onCreate()
, so the TestComponent
is used. When the app is run normally, component
will be set to ApplicationComponent
in onCreate()
.
Approach 2: Mock application
Exposing setComponent
in the application is not great because ideally the application should not have test-specific code. Another approach is to subclass the application in the androidTest
folder and load it during tests via a custom test runner. See blog.sqisland.com/2015/12/mock-application-in-espresso.html for more details.
Mockito
The app has a Clock
class which returns the current time.
public DateTime getNow() { return new DateTime(); }
TestComponent
contains MockClockModule
, which provides Clock
as mocked by Mockito. This way MainActivityTest
can supply a pre-determined date during test.
Mockito.when(clock.getNow()) .thenReturn(new DateTime(2008, 9, 23, 0, 0, 0));
Since we have singleton modules, the same mocked Clock
is supplied to the app. With that, it will display the provided date instead of today's date:
onView(withId(R.id.date)) .check(matches(withText("2008-09-23")));
More
There is a lot more in the repo, including testing activity launch with intent and unit testing with JUnit. Please check it out:
https://github.com/chiuki/android-test-demo
Look at the setComponent branch to see the old code for injection using the setComponent
function.
Master uses mock application and custom test runner for injection.
Also, I am toying with the idea of writing a book on Espresso. Please take a look at the outline and fill in this form if you think I should write it!
Update: I have started publishing Espresso courses! https://gumroad.com/chiuki
Further reading:
Inline coding questions will not be answsered. Instead, ask on StackOverflow and put the link in the comment.
Great post, simple and effective!
ReplyDeleteJust a question: looking at the example you are using the ActivityLauncher but not the Rule annotation, why?
My talk at DroidCon Italy was about this argument, I have an example similar to your:
https://github.com/fabioCollini/TestableAndroidAppsDroidCon15/tree/master/app/src/androidTest/java/it/cosenonjaviste/testableandroidapps/v3
Here you can find the slides:
http://www.slideshare.net/fabio_collini/testable-android-apps-droidcon-italy-2015
MainActivityTest has two test methods: today() where the app just launches, and intent() where it launches with an date specified via intent. If I use a Rule the same intent will be applied to both methods.
Deletehttps://github.com/chiuki/android-test-demo/blob/master/app/src/androidTest/java/com/sqisland/android/test_demo/MainActivityTest.java
You are right! However in this talk http://it.droidcon.com/2015/sessions/effective-android-testing/ Stephan Linzner presented Espresso 2.1, it will contain a test rule similar to the one you used. The first question after the talk was how to use different intents in the same test, I think they'll solve this problem too.
DeleteThat's great news!
DeleteThanks for the post. Makes me see the light on testing dependencies.
ReplyDeleteFor anyone reading this, "We call setComponent() in test, which runs before onCreate()" is the key sentence here :)
Do you know of any way to set the root component in the Application class from an Espresso test *before* the Application.onCreate() is called?
ReplyDeleteIf setComponent() is called inside the @Test method it happens after Application.onCreate() - thus, a root component has already been created once before and the setComponent() call overwrites it and re-injects.
This can be a problem if the 1st root component provided dependencies which themselves did some setup which can't be reversed by the the 2nd root component.
I know that overriding the AndroidJunitRunner can help me achieve this (http://stackoverflow.com/questions/30052601/activitytestrule-how-to-call-code-before-applications-oncreate), but I don't know if there is a way to interact with the runner from inside a @Before or @Test method - to provide custom mocks of module etc.
Take a look at the examples in http://blog.sqisland.com/2015/12/mock-application-in-espresso.html. The mock application gives you the test component, so you have access to the mock modules.
DeleteFrom what I could tell, this only works because your components are @Singletons and only provides @Singleton objects. So, when you inject them in your test files and modify, when they are injected at the real Activities, they will get the modified objects. Perfect for this situation.
ReplyDeleteMy question, and I'm really struggling to get this to work, is: What if you have custom scopes, like @PerActivity, that will provide a different instance to every caller. This way, injecting them to your test files would serve for nothing. How would you approach this situation?
Thanks!
@Chiu_Ki Chan- Great job! I am about to write unit test cases for my code which is very much network intensive. Now wondering which combination of testing frameworks I should use: Dragger, Mockito, Roboelectoic, Espresso, and other?
ReplyDeleteAnyway, your example is great moving away from Dragger just because it has test relevant code. But Dragger is easy to use for me, I don't mind keeping one testing line in code and then commenting it out. I know this is not an elegant solution.
You can use any combination of the testing frameworks you mentioned. The key is to mock the network responses so you are not depending on an external server. Otherwise your tests will fail when the external server is down.
Deletehi chan
ReplyDeleteif i use
multiDexEnabled true
compile 'com.android.support:multidex:1+'
and change
DemoApplication extends MultiDexApplication
you example do not work
Test running startedTest running failed: Instrumentation run failed due to 'java.lang.NoClassDefFoundError'
your code not support multidex?
I am curious, is it a good practice to use Dagger for unit tests? Dagger 2 documentation suggests otherwise: https://google.github.io/dagger/testing.html
ReplyDeleteI agree that you should not use Dagger for unit tests. Here we are using Dagger with Espresso, which is not unit testing.
Delete