5

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;
}
Community
  • 1
  • 1
unlimited101
  • 3,653
  • 4
  • 22
  • 41
  • Dagger is not used for testing. You first have to architecture your classes to use DI, which have the good side effect of making the class easier to test with fakes/mocks of the dependencies. you can either new up the dependencies manually or using a mocking framework. – Nkosi Nov 03 '16 at 16:06
  • My architecture is ready for DI already. I just cannot apply the architecture to the test case. What exactly do you mean by "new up the dependencies manually"? – unlimited101 Nov 03 '16 at 16:19
  • you create a new instance of the `FakeMainInteractor` and inject it into the system under test when instantiating it. Also your fakes and concrete implementations should share a common abstraction. – Nkosi Nov 03 '16 at 16:20
  • @Nkosi But you mean I create `FakeMainInteractor`just using its constructor. And by "inject" you don't mean Dagger's inject but just applying the new object to the system? – unlimited101 Nov 03 '16 at 16:23
  • 2
    Possible duplicate of [Android Unit Tests with Dagger 2](http://stackoverflow.com/questions/29989245/android-unit-tests-with-dagger-2) – Beloo Nov 04 '16 at 08:57

1 Answers1

3

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.

David Rawson
  • 20,912
  • 7
  • 88
  • 124
  • 1
    Does `setApplicationComponent()` really need to return `ApplicationComponent` or should it be `void`? – Code-Apprentice Nov 04 '16 at 22:56
  • @DavidRawson Thanks, that helps alot. But unfortunately the test still uses the `MainInteractor` instead of the `FakeMainInteractor`, although I have implemented and called this `setComponent` methods. What I think is that Components are overwritten by the TestComponents within `AndroidApplication`and `MainActivity`but still the original ones are injected. I have attached that newest edit to my post. – unlimited101 Nov 07 '16 at 15:08
  • @DavidRawson I think my problem is that I don't only have an `ApplicationComponent` but also a `FragmentComponent`. And the latter is not really swapped. But could you please have a look at my new sources how to fix that? https://github.com/unlimited101/PresenterInjection – unlimited101 Nov 07 '16 at 16:19
  • My referenced github repo is outdated. See this post: http://stackoverflow.com/questions/40485911/how-can-i-swap-test-doubles-at-the-scope-of-an-activity-or-a-fragment-using-dagg – unlimited101 Nov 17 '16 at 19:53