Dagger 2 Dependency Injection in Android TestCase

2019-04-30 19:54发布

I have built up an example application (Yes, it is really just an example and doesn’t make much sense but is good for understanding Android clean architecture and dependency injection in Dagger 2). My code is available on github.(Outdated. See this post) The example app just let’s you type in a name in an EditText and if you press the Button you see a message "Hello YourName"

I have three different Components: ApplicationComponent, ActivityComponent and FragmentComponent. FragmentComponent contains three modules:

  • ActivityModule
  • FragmentModule
  • InteractorModule

InteractorModule provides a MainInteractor.

@Module
public class InteractorModule {

    @Provides
    @PerFragment
    MainInteractor provideMainInteractor () {
        return new MainInteractor();
    }
}

In my Activity-UnitTest I want to fake this MainInteractor. This Interactor just has a method public Person createPerson(String name) which can create a Person object. The FakeMainInteractor has the same method but always creates a Person object with the name „Fake Person“, indepent of the parameter you have passed.

public class FakeMainInteractor {
    public Person createPerson(final String name) {
        return new Person("Fake Person");
    }
}

I already createt TestComponents for evey Component I described above. And In TestFragmentComponent I swapped InteractorModule with TestInteractorModule.

@PerFragment
@Component(dependencies = TestApplicationComponent.class, modules = {ActivityModule.class, FragmentModule.class, TestInteractorModule.class})
public interface TestFragmentComponent {
    void inject(MainFragment mainFragment);

    void inject(MainActivity mainActivity);
}

This example is running well in a non-test context. In the MainActivityI have a method called initializeInjector() where I build the FragmentComponent. And onCreate()calls onActivitySetup() which calls initializeInjector() and inject().

public class MainActivity extends BaseActivity implements MainFragment.OnFragmentInteractionListener,
        HasComponent<FragmentComponent> {


    private FragmentComponent fragmentComponent;
    private Fragment currentFragment;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            currentFragment = new MainFragment();
            addFragment(R.id.fragmentContainer, currentFragment);
        }

    }


    private void initializeInjector() {
        this.fragmentComponent = DaggerFragmentComponent.builder()
                .applicationComponent(getApplicationComponent())
                .activityModule(getActivityModule())
                .fragmentModule(getFragmentModule())
                .build();
    }

    @Override
    protected void onActivitySetup() {
        this.initializeInjector();
        fragmentComponent.inject(this);

    }

    @Override
    public void onFragmentInteraction(final Uri uri) {

    }

    @Override public FragmentComponent getComponent() {
        return fragmentComponent;
    }


    public FragmentModule getFragmentModule() {
        return new FragmentModule(currentFragment);
    }
}

This works fine. And my MainActivityTestalso works fine. It tests the typing in of the name and the result of the following button click. But the TextView shows „Hello John“.

public class MainActivityTest implements HasComponent<TestFragmentComponent> {

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

    private MainActivity mActivity;
    private TestFragmentComponent mTestFragmentComponent;


    @Before
    public void setUp() throws Exception {
        mActivity = mActivityRule.getActivity();
    }

    @Test
    public void testMainFragmentLoaded() throws Exception {
        mActivity = mActivityRule.getActivity();
        assertTrue(mActivity.getCurrentFragment() instanceof MainFragment);
    }

    @Test
    public void testOnClick() throws Exception {
        onView(withId(R.id.edittext)).perform(typeText("John"));
        onView(withId(R.id.button)).perform(click());
        onView(withId(R.id.textview_greeting)).check(matches(withText(containsString("Hello John"))));

    }


    @Override
    public TestFragmentComponent getComponent() {
        return mTestFragmentComponent;
    }
}

But as I told I want to use the FakeMainInteractor which would print „Hello Fake Person“. But I don’t know how to build up the dependency graph within the Test. So in test mode I want another graph to be created, using the TestComponents and TestModules instead of the original Components and Modules. So how to do that? How to let the test use FakeMainInteractor?

As I told, I know this example app doesn’t do anything useful. But I would like to understand Testing with Dagger 2. I already read this article. But it just shows how to make the TestComponents and the TestModules. It does not tell how to use a Test-Graph in the Unit Test. How to do that? Can someone provide some example code?

This is not a solution for me, because it uses and older version of Dagger 2 (I use version 2.7) and it does not describe how to wire the TestComponents.

After trying approach by @DavidRawson some of my classes changed their implementation:

public class MainActivityTest{

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

    private MainActivity mActivity;
    private TestApplicationComponent mTestApplicationComponent;
    private TestFragmentComponent mTestFragmentComponent;

    private void initializeInjector() {
        mTestApplicationComponent = DaggerTestApplicationComponent.builder()
                .applicationModule(new ApplicationModule(getApp()))
                .build();

        getApp().setApplicationComponent(mTestApplicationComponent);

        mTestFragmentComponent = DaggerTestFragmentComponent.builder()
                .testApplicationComponent(mTestApplicationComponent)
                .activityModule(mActivity.getActivityModule())
                .testInteractorModule(new TestInteractorModule())
                .build();

        mActivity.setFragmentComponent(mTestFragmentComponent);

        mTestApplicationComponent.inject(this);
        mTestFragmentComponent.inject(this);

    }

    public AndroidApplication getApp() {
        return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
    }

    @Before
    public void setUp() throws Exception {
        mActivity = mActivityRule.getActivity();
        initializeInjector();
    }

    @Test
    public void testMainFragmentLoaded() throws Exception {
        mActivity = mActivityRule.getActivity();
        assertTrue(mActivity.getCurrentFragment() instanceof MainFragment);
    }

    @Test
    public void testOnClick() throws Exception {
        onView(withId(R.id.edittext)).perform(typeText("John"));
        onView(withId(R.id.button)).perform(click());
        onView(withId(R.id.textview_greeting)).check(matches(withText(containsString("Hello John"))));
    }

}

MainActivityowns the following new method:

@Override
public void setFragmentComponent(final FragmentComponent fragmentComponent) {
    Log.w(TAG, "Only call this method to swap test doubles");
    this.fragmentComponent = fragmentComponent;
}

AndroidApplication owns:

public void setApplicationComponent(ApplicationComponent applicationComponent) {
    Log.w(TAG, "Only call this method to swap test doubles");
    this.applicationComponent = applicationComponent;
}

1条回答
2楼-- · 2019-04-30 20:10

You can write a setter method in the Application to override the root Component

Modify your current Application class by adding this method:

public class AndroidApplication extends Application {

    @VisibleForTesting
    public void setApplicationComponent(ApplicationComponent applicationComponent) {
        Log.w(TAG, "Only call this method to swap test doubles");
        this.applicationComponent = applicationComponent;
    }
}

now in your test setup method, you can swap the real root Component with the fake one:

@Before
public void setUp() throws Exception {
    TestApplicationComponent component = 
      DaggerTestApplicationComponent.builder()
        .applicationModule(new TestApplicationModule()).build();

    getApp().setComponent(component); 

}

private AndroidApplication getApp() {
    return (AndroidApplication) InstrumentationRegistry.getInstrumentation()
      .getTargetContext().getApplicationContext();
}

If you are using dependent subcomponents, you will probably have to, again, write a method called setComponent inside your BaseActivity. Please note that adding public getters and setters can be, in general, bad OO design practice but this is currently the simplest solution for performing hermetic tests using Dagger 2. These methods are documented here.

查看更多
登录 后发表回答