Getting Dagger to inject mock objects when doing Espresso functional testing for Android

Kaushik Gopal picture Kaushik Gopal · Apr 23, 2014 · Viewed 11.7k times · Source

I've recently gone whole-hog with Dagger because the concept of DI makes complete sense. One of the nicer "by-products" of DI (as Jake Wharton put in one of his presentations) is easier testability.

So now I'm basically using Espresso to do some functional testing, and I want to be able to inject dummy/mock data to the application and have the activity show them up. I'm guessing since, this is one of the biggest advantages of DI, this should be a relatively simple ask. For some reason though, I can't seem to wrap my head around it. Any help would be much appreciated. Here's what I have so far (I've written up an example that reflects my current setup):

public class MyActivity
    extends MyBaseActivity {

    @Inject Navigator _navigator;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.get(this).inject(this);

        // ...

        setupViews();
    }

    private void setupViews() {
        myTextView.setText(getMyLabel());
    }

    public String getMyLabel() {
        return _navigator.getSpecialText(); // "Special Text"
    }
}

These are my dagger modules:

// Navigation Module

@Module(library = true)
public class NavigationModule {

    private Navigator _nav;

    @Provides
    @Singleton
    Navigator provideANavigator() {
        if (_nav == null) {
            _nav = new Navigator();
        }
        return _nav;
    }
}

// App level module

@Module(
    includes = { SessionModule.class, NavigationModule.class },
    injects = { MyApplication.class,
                MyActivity.class,
                // ...
})
public class App {
    private final Context _appContext;
    AppModule(Context appContext) {
        _appContext = appContext;
    }
    // ...
}

In my Espresso Test, I'm trying to insert a mock module like so:

public class MyActivityTest
    extends ActivityInstrumentationTestCase2<MyActivity> {

    public MyActivityTest() {
        super(MyActivity.class);
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        ObjectGraph og = ((MyApplication) getActivity().getApplication()).getObjectGraph().plus(new TestNavigationModule());
        og.inject(getActivity());
    }

    public void test_SeeSpecialText() {
        onView(withId(R.id.my_text_view)).check(matches(withText(
            "Special Dummy Text")));
    }

    @Module(includes = NavigationModule.class,
            injects = { MyActivityTest.class, MyActivity.class },
            overrides = true,
            library = true)
    static class TestNavigationModule {

        @Provides
        @Singleton
        Navigator provideANavigator() {
            return new DummyNavigator(); // that returns "Special Dummy Text"
        }
    }
}

This is not working at all. My Espresso tests run, but the TestNavigationModule is completely ignored... arr... :(

What am I doing wrong? Is there a better approach to mocking modules out with Espresso? I've searched and seen examples of Robolectric, Mockito etc. being used. But I just want pure Espresso tests and need to swap out a module with my mock one. How should i be doing this?

EDIT:

So I went with @user3399328 approach of having a static test module list definition, checking for null and then adding it in my Application class. I'm still not getting my Test injected version of the class though. I have a feeling though, its probably something wrong with dagger test module definition, and not my espresso lifecycle. The reason I'm making the assumption is that I add debug statements and find that the static test module is non-empty at time of injection in the application class. Could you point me to a direction of what I could possibly be doing wrong. Here are code snippets of my definitions:

MyApplication:

@Override
public void onCreate() {
    // ...
    mObjectGraph = ObjectGraph.create(Modules.list(this));
    // ...   
}

Modules:

public class Modules {

    public static List<Object> _testModules = null;

    public static Object[] list(MyApplication app) {
        //        return new Object[]{ new AppModule(app) };
        List<Object> modules = new ArrayList<Object>();
        modules.add(new AppModule(app));

        if (_testModules == null) {
            Log.d("No test modules");
        } else {
            Log.d("Test modules found");
        }

        if (_testModules != null) {
            modules.addAll(_testModules);
        }

        return modules.toArray();
    }
}   

Modified test module within my test class:

@Module(overrides = true, library = true)
public static class TestNavigationModule {

    @Provides
    @Singleton
    Navigator provideANavigator()() {
        Navigator navigator = new Navigator();
        navigator.setSpecialText("Dummy Text");
        return navigator;
    }
}

Answer

pmellaaho picture pmellaaho · Aug 29, 2015

With Dagger 2 and Espresso 2 things have indeed improved. This is how a test case could look like now. Notice that ContributorsModel is provided by Dagger. The full demo available here: https://github.com/pmellaaho/RxApp

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

ContributorsModel mModel;

@Singleton
@Component(modules = MockNetworkModule.class)
public interface MockNetworkComponent extends RxApp.NetworkComponent {
}

@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
        MainActivity.class,
        true,     // initialTouchMode
        false);   // launchActivity.

@Before
public void setUp() {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    RxApp app = (RxApp) instrumentation.getTargetContext()
            .getApplicationContext();

    MockNetworkComponent testComponent = DaggerMainActivityTest_MockNetworkComponent.builder()
            .mockNetworkModule(new MockNetworkModule())
            .build();
    app.setComponent(testComponent);
    mModel = testComponent.contributorsModel();
}

@Test
public void listWithTwoContributors() {

    // GIVEN
    List<Contributor> tmpList = new ArrayList<>();
    tmpList.add(new Contributor("Jesse", 600));
    tmpList.add(new Contributor("Jake", 200));

    Observable<List<Contributor>> testObservable = Observable.just(tmpList);

    Mockito.when(mModel.getContributors(anyString(), anyString()))
            .thenReturn(testObservable);

    // WHEN
    mActivityRule.launchActivity(new Intent());
    onView(withId(R.id.startBtn)).perform(click());

    // THEN
    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0))
            .check(matches(hasDescendant(withText("Jesse"))));

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0))
            .check(matches(hasDescendant(withText("600"))));

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1))
            .check(matches(hasDescendant(withText("Jake"))));

    onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1))
            .check(matches(hasDescendant(withText("200"))));
}