Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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;
    }
}
like image 312
Kaushik Gopal Avatar asked Apr 23 '14 01:04

Kaushik Gopal


3 Answers

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"))));
}
like image 146
pmellaaho Avatar answered Nov 15 '22 21:11

pmellaaho


Your approach doesn't work because it only happens once, and as Matt mentioned, when the activity's real injection code runs, it will wipe out any variables injected by your special object graph.

There are two ways to get this to work.

The quick way: make a public static variable in your activity so a test can assign an override module and have the actual activity code always include this module if it's not null (which will only happen in tests). It's similar to my answer here just for your activity base class instead of application.

The longer, probably better way: refactor your code so that all activity injection (and more importantly graph creation) happens in one class, something like ActivityInjectHelper. In your test package, create another class named ActivityInjectHelper with the exact same package path that implements the same methods, except also plusses your test modules. Because test classes are loaded first, your application will execute with the testing ActivityInjectHelper. Again it's similar to my answer here just for a different class.

UPDATE:

I see you've posted more code and it's close to working, but no cigar. For both activities and applications, the test module needs to be snuck in before onCreate() runs. When dealing with activity object graphs, anytime before the test's getActivity() is fine. When dealing with applications, it's a bit harder because onCreate() has already been called by the time setUp() runs. Luckily, doing it in the test's constructor works - the application hasn't been created at that point. I briefly mention this in my first link.

like image 8
user3399328 Avatar answered Nov 15 '22 20:11

user3399328


The call to getActivity will actually start your activity calling onCreate in the process which means you won't be getting your test modules added to the graph in time to be used. Using activityInstrumentationTestcase2 you can't really inject properly at the activity scope. I've worked around this by using my application to provide dependencies to my activities and then inject mock objects into it which the activities will use. It's not ideal but it works. You can use an event bus like Otto to help provide dependencies.

like image 1
Matt Wolfe Avatar answered Nov 15 '22 21:11

Matt Wolfe