How can I change the test doubles in the scope or fragment using dagger 2?

EDIT: Beware! I deleted the old repository mentioned in this question. See my own answer to a question about a possible solution and do not hesitate to improve it!

I refer to my post here . Now I have come a little further. I also refer to my two branches as part of my github project:

  • Experimental [industry number. 1] (repository deleted)
  • Experimental [industry number. 2] (repository deleted)

In an old post, I tried swapping components for test components in the Instrumentation test. This works now if I have an ApplicationComponent located in a singleton scope. But it does not work if I have an ActivityComponent with a self- @PerActivity scope @PerActivity . The problem is not the scope, but the replacement of the component with TestComponent.

My ActivityComponent has an ActivityModule :

 @PerActivity @Component(modules = ActivityModule.class) public interface ActivityComponent { // TODO: Comment this out for switching back to the old approach void inject(MainFragment mainFragment); // TODO: Leave that for witching to the new approach void inject(MainActivity mainActivity); } 

ActivityModule provides MainInteractor

 @Module public class ActivityModule { @Provides @PerActivity MainInteractor provideMainInteractor () { return new MainInteractor(); } } 

My TestActivityComponent uses TestActivityModule :

 @PerActivity @Component(modules = TestActivityModule.class) public interface TestActivityComponent extends ActivityComponent { void inject(MainActivityTest mainActivityTest); } 

TestActvityModule provides a FakeInteractor :

 @Module public class TestActivityModule { @Provides @PerActivity MainInteractor provideMainInteractor () { return new FakeMainInteractor(); } } 

My MainActivity has a getComponent() method and a setComponent() method. Using the latter, you can swap a component for a test component in the Instrumentation Test. Here is the activity:

 public class MainActivity extends BaseActivity implements MainFragment.OnFragmentInteractionListener { private static final String TAG = "MainActivity"; private Fragment currentFragment; private ActivityComponent activityComponent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initializeInjector(); if (savedInstanceState == null) { currentFragment = new MainFragment(); addFragment(R.id.fragmentContainer, currentFragment); } } private void initializeInjector() { Log.i(TAG, "injectDagger initializeInjector()"); activityComponent = DaggerActivityComponent.builder() .activityModule(new ActivityModule()) .build(); activityComponent.inject(this); } @Override public void onFragmentInteraction(final Uri uri) { } ActivityComponent getActivityComponent() { return activityComponent; } @VisibleForTesting public void setActivityComponent(ActivityComponent activityComponent) { Log.w(TAG, "injectDagger Only call this method to swap test doubles"); this.activityComponent = activityComponent; } } 

As you can see, this exercise uses MainFragment . The component is introduced into the onCreate() fragment:

 public class MainFragment extends BaseFragment implements MainView { private static final String TAG = "MainFragment"; @Inject MainPresenter mainPresenter; private View view; public MainFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "injectDagger onCreate()"); super.onCreate(savedInstanceState); // TODO: That approach works // ((AndroidApplication)((MainActivity) getActivity()).getApplication()).getApplicationComponent().inject(this); // TODO: This approach is NOT working, see MainActvityTest ((MainActivity) getActivity()).getActivityComponent().inject(this); } } 

And then in the test, I change the ActivityComponent to TestApplicationComponent :

 public class MainActivityTest{ @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule(MainActivity.class, true, false); private MainActivity mActivity; private TestActivityComponent mTestActivityComponent; // TODO: That approach works // private TestApplicationComponent mTestApplicationComponent; // // private void initializeInjector() { // mTestApplicationComponent = DaggerTestApplicationComponent.builder() // .testApplicationModule(new TestApplicationModule(getApp())) // .build(); // // getApp().setApplicationComponent(mTestApplicationComponent); // mTestApplicationComponent.inject(this); // } // TODO: This approach does NOT work because mActivity.setActivityComponent() is called after MainInteractor has already been injected! private void initializeInjector() { mTestActivityComponent = DaggerTestActivityComponent.builder() .testActivityModule(new TestActivityModule()) .build(); mActivity.setActivityComponent(mTestActivityComponent); mTestActivityComponent.inject(this); } public AndroidApplication getApp() { return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); } // TODO: That approach works // @Before // public void setUp() throws Exception { // // initializeInjector(); // mActivityRule.launchActivity(null); // mActivity = mActivityRule.getActivity(); // } // TODO: That approach does not works because mActivity.setActivityComponent() is called after MainInteractor has already been injected! @Before public void setUp() throws Exception { mActivityRule.launchActivity(null); mActivity = mActivityRule.getActivity(); initializeInjector(); } @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")))); } } 

An activity test is running, but the wrong Component . This is because operations and fragments of onCreate() are performed before the component is replaced.

As you can see, I have an old comment, I bound ApplicationComponent to the application class. This works because I can create a dependency before the action begins. But now with ActivityComponent I need to start this activity before the injector initializes. Because otherwise I could not establish

 mActivity.setActivityComponent(mTestActivityComponent); 

because mActivity will be null if activity mActivity after the injector initializes. (See MainActivityTest )

So, how could I intercept MainActivity and MainFragment to use TestActivityComponent ?

+1
source share
1 answer

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:

 /** * Own JUnit runner for intercepting the ActivityComponent injection and swapping the * ActivityComponent with the TestActivityComponent */ 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(); //Instantiate and insert the fragment into the container layout FragmentManager manager = getActivity().getSupportFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); transaction.replace(R.id.fragmentContainer, mFragment); transaction.commit(); } public F getFragment() { return mFragment; } } 

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 :

 /** * Because neither the Activity nor the ActivityTest can hold the TestActivityComponent (due to * runtime order problems we need to hold it statically **/ public class TestFragmentComponentHolder { private static TestFragmentComponent sComponent; private static ComponentCreator sCreator; public interface ComponentCreator { TestFragmentComponent createComponent(Fragment fragment); } /** * Configures an ComponentCreator that is used to create an activity graph. Call that in @Before. * * @param creator The creator */ public static void setCreator(ComponentCreator creator) { sCreator = creator; } /** * Releases the static instances of our creator and graph. Call that in @After. */ public static void release() { 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 */ @NonNull public static TestFragmentComponent getComponent(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 */ public static boolean hasComponentCreator() { return sCreator != null; } /** * Returns a previously instantiated {@link TestFragmentComponent}. * * @throws IllegalStateException if none has been instantiated */ @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; // TODO: Rename and change types and number of parameters public static MainFragment newInstance() { MainFragment fragment = new MainFragment(); return fragment; } public MainFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //((MainActivity)getActivity()).getComponent().inject(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { view = inflater.inflate(R.layout.fragment_main, container, false); return view; } public void onClick(final String s) { mainPresenter.onClick(s); } @Override public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getComponent().inject(this); final EditText editText = (EditText) view.findViewById(R.id.edittext); Button button = (Button) view.findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { MainFragment.this.onClick(editText.getText().toString()); } }); mainPresenter.attachView(this); } @Override public void updatePerson(final Person person) { TextView textView = (TextView) view.findViewById(R.id.textview_greeting); textView.setText("Hello " + person.getName()); } @Override public void onDestroy() { super.onDestroy(); mainPresenter.detachView(); } public interface OnFragmentInteractionListener { void onFragmentInteraction(Uri uri); } } 

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!

0
source

Source: https://habr.com/ru/post/1259238/


All Articles