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

12,260

Solution 1

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.

Solution 2

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"))));
}

Solution 3

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.

Share:
12,260
KG -
Author by

KG -

http://jkl.gg

Updated on June 07, 2022

Comments

  • KG -
    KG - almost 2 years

    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;
        }
    }
    
  • KG -
    KG - about 10 years
    This is the way to go. Ingenious way of getting around the limitation. Rock on sir!
  • Johan Bilien
    Johan Bilien about 9 years
    When I try to inject my test modules in the constructor of my ActivityInstrumentationTestCase2 subclass, it doesn't work because the Application has already been instantiated and onCreate has already been called. So I still haven't found a good way of declaring my test modules when running tests
  • Ben
    Ben over 8 years
    I am currently trying out @user3399328's second solution (providing another version of ActivityInjectHelper in my test sources). Have you found a way to get this to work with Gradle? I keep running into Duplicate class found in the file [filename] errors that pop up in Android Studio.
  • mmm111mmm
    mmm111mmm over 7 years
    This is the best way I've found, too. 1) expose the DI container on your Application 2) make the ActivityTestRule not automatically launch your app 3) change the DI container in the test method (or setup) 4) start your app manually 5) test.