Skip to content Skip to sidebar Skip to footer

How Can I Swap Test Doubles At The Scope Of An Activity Or A Fragment Using Dagger 2?

EDIT: Watch out! I have deleted the old repository reffered to in this question. See my own answer to the question for a possible solution and feel free to improve it! I am referin

Solution 1:

Now I found out by mixing some examples how to exchange an Activity-scoped component and a Fragment-scoped component. In this post I will show you how to do both. But I will describe in more detail how to swap a Fragment-scoped component during an InstrumentationTest. My total code is hosted on github. You can run the MainFragmentTest class but be aware that you have to set de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner as TestRunner in Android Studio.

Now I describe shortly what to do to swap an Interactor by a Fake Interactor. In the example I try to respect clean architecture as much as possible. But they may be some small things which break this architecture a bit. So feel free to improve.

So, let's start. At first you need an own JUnitRunner:

/**
 * Own JUnit runner for intercepting the ActivityComponent injection and swapping the
 * ActivityComponent with the TestActivityComponent
 */publicclassAndroidApplicationJUnitRunnerextendsAndroidJUnitRunner {
    @Overridepublic Application newApplication(ClassLoader classLoader, String className, Context context)throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        returnsuper.newApplication(classLoader, TestAndroidApplication.class.getName(), context);
    }

    @Overridepublic Activity newActivity(ClassLoader classLoader, String className, Intent intent)throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Activityactivity=super.newActivity(classLoader, className, intent);
        return swapActivityGraph(activity);
    }

    @SuppressWarnings("unchecked")private Activity swapActivityGraph(Activity activity) {
        if (!(activity instanceof HasComponent) || !TestActivityComponentHolder.hasComponentCreator()) {
            return activity;
        }

        ((HasComponent<ActivityComponent>) activity).
                setComponent(TestActivityComponentHolder.getComponent(activity));

        return activity;
    }
}

In swapActivityGraph() I create an alternative TestActivityGraph for the Activity before(!) the Activity is created when running the test. Then we have to create a TestFragmentComponent:

@PerFragment@Component(modules = TestFragmentModule.class)
public interface TestFragmentComponent extends FragmentComponent{
    voidinject(MainActivityTest mainActivityTest);

    voidinject(MainFragmentTest mainFragmentTest);
}

This component lives in a Fragment-scope. It has a module:

@Module
public class TestFragmentModule {
    @Provides@PerFragment
    MainInteractor provideMainInteractor () {
        returnnewFakeMainInteractor();
    }
}

The original FragmentModule looks like that:

@Module
public class FragmentModule {
    @Provides@PerFragment
    MainInteractor provideMainInteractor () {
        returnnewMainInteractor();
    }
}

You see I use a MainInteractor and a FakeMainInteractor. They both look like that:

publicclassMainInteractor {
    privatestaticfinalStringTAG="MainInteractor";

    publicMainInteractor() {
        Log.i(TAG, "constructor");
    }

    public Person createPerson(final String name) {
        returnnewPerson(name);
    }
}


publicclassFakeMainInteractorextendsMainInteractor {
    privatestaticfinalStringTAG="FakeMainInteractor";

    publicFakeMainInteractor() {
        Log.i(TAG, "constructor");
    }

    public Person createPerson(final String name) {
        returnnewPerson("Fake Person");
    }
}

Now we use a self-defined FragmentTestRule for testing the Fragment independent from the Activity which contains it in production:

publicclassFragmentTestRule<F extendsFragment> extendsActivityTestRule<TestActivity> {
    privatestaticfinalStringTAG="FragmentTestRule";
    privatefinal Class<F> mFragmentClass;
    private F mFragment;

    publicFragmentTestRule(final Class<F> fragmentClass) {
        super(TestActivity.class, true, false);
        mFragmentClass = fragmentClass;
    }

    @OverrideprotectedvoidbeforeActivityLaunched() {
        super.beforeActivityLaunched();
        try {
            mFragment = mFragmentClass.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @OverrideprotectedvoidafterActivityLaunched() {
        super.afterActivityLaunched();

        //Instantiate and insert the fragment into the container layoutFragmentManagermanager= getActivity().getSupportFragmentManager();
        FragmentTransactiontransaction= manager.beginTransaction();

        transaction.replace(R.id.fragmentContainer, mFragment);
        transaction.commit();
    }


    public F getFragment() {
        return mFragment;
    }
}

That TestActivity is very simple:

publicclassTestActivityextendsBaseActivityimplementsHasComponent<ActivityComponent> {

    @OverrideprotectedvoidonCreate(@Nullablefinal Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FrameLayoutframeLayout=newFrameLayout(this);
        frameLayout.setId(R.id.fragmentContainer);
        setContentView(frameLayout);
    }
}

But now how to swap the components? There are several small tricks to achieve that. At first we need a holder class for holding the TestFragmentComponent:

/**
 * Because neither the Activity nor the ActivityTest can hold the TestActivityComponent (due to
 * runtime order problems we need to hold it statically
 **/publicclassTestFragmentComponentHolder {
    privatestaticTestFragmentComponent sComponent;
    privatestaticComponentCreator sCreator;

    publicinterfaceComponentCreator {
        TestFragmentComponentcreateComponent(Fragment fragment);
    }

    /**
     * Configures an ComponentCreator that is used to create an activity graph. Call that in @Before.
     *
     * @param creator The creator
     */publicstaticvoidsetCreator(ComponentCreator creator) {
        sCreator = creator;
    }

    /**
     * Releases the static instances of our creator and graph. Call that in @After.
     */publicstaticvoidrelease() {
        sCreator = null;
        sComponent = null;
    }

    /**
     * Returns the {@link TestFragmentComponent} or creates a new one using the registered {@link
     * ComponentCreator}
     *
     * @throws IllegalStateException if no creator has been registered before
     */@NonNullpublicstaticTestFragmentComponentgetComponent(Fragment fragment) {
        if (sComponent == null) {
            checkRegistered(sCreator != null, "no creator registered");
            sComponent = sCreator.createComponent(fragment);
        }
        return sComponent;
    }

    /**
     * Returns true if a custom activity component creator was configured for the current test run,
     * false otherwise
     */publicstaticbooleanhasComponentCreator() {
        return sCreator != null;
    }

    /**
     * Returns a previously instantiated {@link TestFragmentComponent}.
     *
     * @throws IllegalStateException if none has been instantiated
     */@NonNullpublicstaticTestFragmentComponentgetComponent() {
        checkRegistered(sComponent != null, "no component created");
        return sComponent;
    }
}

The second trick is to use the holder to register the component before the fragment is even created. Then we launch the TestActivity with our FragmentTestRule. Now comes the third trick which is timing-dependent and does not always run correctly. Directly after launching the activity we get the Fragment instance by asking the FragmentTestRule. Then we swap the component, using the TestFragmentComponentHolder and inject the Fragment graph. The forth trick is we just wait for about 2 seconds for the Fragment to be created. And within the Fragment we make our component injection in onViewCreated(). Because then we don't inject the component to early because onCreate() and onCreateView() are called before. So here is our MainFragment:

publicclassMainFragmentextendsBaseFragmentimplementsMainView {

    privatestaticfinalStringTAG="MainFragment";
    @Inject
    MainPresenter mainPresenter;
    private View view;

    // TODO: Rename and change types and number of parameterspublicstatic MainFragment newInstance() {
        MainFragmentfragment=newMainFragment();
        return fragment;
    }

    publicMainFragment() {
        // Required empty public constructor
    }

    @OverridepublicvoidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //((MainActivity)getActivity()).getComponent().inject(this);
    }

    @Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_main, container, false);
        return view;
    }

    publicvoidonClick(final String s) {
        mainPresenter.onClick(s);
    }

    @OverridepublicvoidonViewCreated(final View view, @Nullablefinal Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        getComponent().inject(this);

        finalEditTexteditText= (EditText) view.findViewById(R.id.edittext);
        Buttonbutton= (Button) view.findViewById(R.id.button);
        button.setOnClickListener(newView.OnClickListener() {
            @OverridepublicvoidonClick(final View v) {
                MainFragment.this.onClick(editText.getText().toString());
            }
        });
        mainPresenter.attachView(this);
    }

    @OverridepublicvoidupdatePerson(final Person person) {
        TextViewtextView= (TextView) view.findViewById(R.id.textview_greeting);
        textView.setText("Hello " + person.getName());
    }

    @OverridepublicvoidonDestroy() {
        super.onDestroy();
        mainPresenter.detachView();
    }

    publicinterfaceOnFragmentInteractionListener {
        voidonFragmentInteraction(Uri uri);
    }
}

And all the steps (second to forth trick) which I described before can be found in the @Before annotated setUp()-Method in this MainFragmentTest class:

publicclassMainFragmentTestimplementsInjectsComponent<TestFragmentComponent>, TestFragmentComponentHolder.ComponentCreator {

    privatestaticfinalStringTAG="MainFragmentTest";
    @Rulepublic FragmentTestRule<MainFragment> mFragmentTestRule = newFragmentTestRule<>(MainFragment.class);

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

    @BeforepublicvoidsetUp()throws Exception {
        TestFragmentComponentHolder.setCreator(this);

        mFragmentTestRule.launchActivity(null);

        MainFragmentfragment= mFragmentTestRule.getFragment();

        if (!(fragment instanceof HasComponent) || !TestFragmentComponentHolder.hasComponentCreator()) {
            return;
        } else {
            ((HasComponent<FragmentComponent>) fragment).
                    setComponent(TestFragmentComponentHolder.getComponent(fragment));

            injectFragmentGraph();

            waitForFragment(R.id.fragmentContainer, 2000);
        }
    }

    @AfterpublicvoidtearDown()throws  Exception {
        TestFragmentComponentHolder.release();
        mFragmentTestRule = null;
    }

    @SuppressWarnings("unchecked")privatevoidinjectFragmentGraph() {
        ((InjectsComponent<TestFragmentComponent>) this).injectComponent(TestFragmentComponentHolder.getComponent());
    }

    protected Fragment waitForFragment(@IdResint id, int timeout) {
        longendTime= SystemClock.uptimeMillis() + timeout;
        while (SystemClock.uptimeMillis() <= endTime) {

            Fragmentfragment= mFragmentTestRule.getActivity().getSupportFragmentManager().findFragmentById(id);
            if (fragment != null) {
                return fragment;
            }
        }
        returnnull;
    }

    @Overridepublic TestFragmentComponent createComponent(final Fragment fragment) {
        return DaggerTestFragmentComponent.builder()
                .testFragmentModule(newTestFragmentModule())
                .build();
    }

    @TestpublicvoidtestOnClick_Fake()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 Fake"))));
    }

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


    @OverridepublicvoidinjectComponent(final TestFragmentComponent component) {
        component.inject(this);
    }
}

Except from the timing problem. This test runs in my environment in 10 of 10 test runs on an emulated Android with API Level 23. And it runs in 9 of 10 test runs on a real Samsung Galaxy S5 Neo device with Android 6.

As I wrote above you can download the whole example from github and feel free to improve if you find a way to fix the little timing problem.

That's it!

Post a Comment for "How Can I Swap Test Doubles At The Scope Of An Activity Or A Fragment Using Dagger 2?"