Now I learned by mixing some examples how to exchange a component with a scope and a component with fragments. In this post I will show you how to do both. But I will talk in more detail about how to exchange a component with fragments during InstrumentationTest. My generic code is posted on github . You can run the MainFragmentTest class, but keep in mind that you need to set de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner as de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner in Android Studio.
Now I am briefly describing what to do to trade Interactor for Fake Interactor . In this example, I try to respect clean architecture as much as possible. But these may be small things that destroy this architecture a bit. So feel free to improve.
So let's get started. First you need your own JUnitRunner:
public class AndroidApplicationJUnitRunner extends AndroidJUnitRunner { @Override public Application newApplication(ClassLoader classLoader, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(classLoader, TestAndroidApplication.class.getName(), context); } @Override public Activity newActivity(ClassLoader classLoader, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Activity activity = 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 Activity before (!) When the action is executed when the test starts. Then we need to create a TestFragmentComponent :
@PerFragment @Component(modules = TestFragmentModule.class) public interface TestFragmentComponent extends FragmentComponent{ void inject(MainActivityTest mainActivityTest); void inject(MainFragmentTest mainFragmentTest); }
This component is in the region of the fragment. It has a module:
@Module public class TestFragmentModule { @Provides @PerFragment MainInteractor provideMainInteractor () { return new FakeMainInteractor(); } }
The original FragmentModule as follows:
@Module public class FragmentModule { @Provides @PerFragment MainInteractor provideMainInteractor () { return new MainInteractor(); } }
You see that I am using MainInteractor and FakeMainInteractor . They both look like this:
public class MainInteractor { private static final String TAG = "MainInteractor"; public MainInteractor() { Log.i(TAG, "constructor"); } public Person createPerson(final String name) { return new Person(name); } } public class FakeMainInteractor extends MainInteractor { private static final String TAG = "FakeMainInteractor"; public FakeMainInteractor() { Log.i(TAG, "constructor"); } public Person createPerson(final String name) { return new Person("Fake Person"); } }
Now we use the self- FragmentTestRule to test the Fragment independent of the Activity that contains it in production:
public class FragmentTestRule<F extends Fragment> extends ActivityTestRule<TestActivity> { private static final String TAG = "FragmentTestRule"; private final Class<F> mFragmentClass; private F mFragment; public FragmentTestRule(final Class<F> fragmentClass) { super(TestActivity.class, true, false); mFragmentClass = fragmentClass; } @Override protected void beforeActivityLaunched() { super.beforeActivityLaunched(); try { mFragment = mFragmentClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } @Override protected void afterActivityLaunched() { super.afterActivityLaunched();
This TestActivity very simple:
public class TestActivity extends BaseActivity implements HasComponent<ActivityComponent> { @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); FrameLayout frameLayout = new FrameLayout(this); frameLayout.setId(R.id.fragmentContainer); setContentView(frameLayout); } }
But now how to change components? There are several small tricks for this. First, we need a holder class to place the TestFragmentComponent :
public class TestFragmentComponentHolder { private static TestFragmentComponent sComponent; private static ComponentCreator sCreator; public interface ComponentCreator { TestFragmentComponent createComponent(Fragment fragment); } public static void setCreator(ComponentCreator creator) { sCreator = creator; } public static void release() { sCreator = null; sComponent = null; } @NonNull public static TestFragmentComponent getComponent(Fragment fragment) { if (sComponent == null) { checkRegistered(sCreator != null, "no creator registered"); sComponent = sCreator.createComponent(fragment); } return sComponent; } public static boolean hasComponentCreator() { return sCreator != null; } @NonNull public static TestFragmentComponent getComponent() { 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 will run TestActivity using our FragmentTestRule . Now comes the third trick, which depends on the time and does not always work correctly. Immediately after starting the operation, we get an instance of Fragment by setting the FragmentTestRule . Then we change the component using TestFragmentComponentHolder and introduce the Fragment graph. The fourth trick - we just wait about 2 seconds to create the Fragment. And inside Fragment, we insert our component in onViewCreated() . Because then we do not introduce the component earlier, because onCreate() and onCreateView() are called earlier. So here is our MainFragment :
public class MainFragment extends BaseFragment implements MainView { private static final String TAG = "MainFragment"; @Inject MainPresenter mainPresenter; private View view;
And all the steps (the second or fourth trick) that I described earlier can be found in the @Before annotated setUp() method in this MainFragmentTest class:
public class MainFragmentTest implements InjectsComponent<TestFragmentComponent>, TestFragmentComponentHolder.ComponentCreator { private static final String TAG = "MainFragmentTest"; @Rule public FragmentTestRule<MainFragment> mFragmentTestRule = new FragmentTestRule<>(MainFragment.class); public AndroidApplication getApp() { return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); } @Before public void setUp() throws Exception { TestFragmentComponentHolder.setCreator(this); mFragmentTestRule.launchActivity(null); MainFragment fragment = mFragmentTestRule.getFragment(); if (!(fragment instanceof HasComponent) || !TestFragmentComponentHolder.hasComponentCreator()) { return; } else { ((HasComponent<FragmentComponent>) fragment). setComponent(TestFragmentComponentHolder.getComponent(fragment)); injectFragmentGraph(); waitForFragment(R.id.fragmentContainer, 2000); } } @After public void tearDown() throws Exception { TestFragmentComponentHolder.release(); mFragmentTestRule = null; } @SuppressWarnings("unchecked") private void injectFragmentGraph() { ((InjectsComponent<TestFragmentComponent>) this).injectComponent(TestFragmentComponentHolder.getComponent()); } protected Fragment waitForFragment(@IdRes int id, int timeout) { long endTime = SystemClock.uptimeMillis() + timeout; while (SystemClock.uptimeMillis() <= endTime) { Fragment fragment = mFragmentTestRule.getActivity().getSupportFragmentManager().findFragmentById(id); if (fragment != null) { return fragment; } } return null; } @Override public TestFragmentComponent createComponent(final Fragment fragment) { return DaggerTestFragmentComponent.builder() .testFragmentModule(new TestFragmentModule()) .build(); } @Test public void testOnClick_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")))); } @Test public void testOnClick_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")))); } @Override public void injectComponent(final TestFragmentComponent component) { component.inject(this); } }
Except for synchronization issues. This test runs in my environment in 10 out of 10 test runs on emulated Android with API level 23. And it works in 9 out of 10 test runs on a real Samsung Galaxy S5 Neo device with Android 6.
As I wrote above, you can download the entire example from github and feel free to improve if you find a way to fix a bit of temporary problems.
What is it!