Thursday, April 9, 2015

Dagger 2 + Espresso 2 + Mockito

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:

13 comments:

Inline coding questions will not be answsered. Instead, ask on StackOverflow and put the link in the comment.

  1. Great post, simple and effective!
    Just 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

    ReplyDelete
    Replies
    1. 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.

      https://github.com/chiuki/android-test-demo/blob/master/app/src/androidTest/java/com/sqisland/android/test_demo/MainActivityTest.java

      Delete
    2. 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.

      Delete
  2. Thanks for the post. Makes me see the light on testing dependencies.
    For anyone reading this, "We call setComponent() in test, which runs before onCreate()" is the key sentence here :)

    ReplyDelete
  3. 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?

    If 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.

    ReplyDelete
    Replies
    1. 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.

      Delete
  4. From 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.

    My 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!

    ReplyDelete
  5. @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?

    Anyway, 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.

    ReplyDelete
    Replies
    1. 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.

      Delete
  6. hi chan
    if 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?

    ReplyDelete
  7. 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

    ReplyDelete
    Replies
    1. I agree that you should not use Dagger for unit tests. Here we are using Dagger with Espresso, which is not unit testing.

      Delete