How Can I Swap Test Doubles At The Scope Of An Activity Or A Fragment Using Dagger 2?
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?"