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 refering to my post here. Now I came a little further. I am also refering to my two branches within my github Project:
- Experimental [branch no. 1] (repository deleted)
- Experimental [branch no. 2] (repository deleted)
In the old post I tried to swap components to test-components within an Instrumentation Test. This works now if I have an ApplicationComponent
, being in singleton scope. But it does not work if I have an ActivityComponent
with a self defined @PerActivity
scope. The problem is not the scope but the swapping of the Component to the 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 a MainInteractor
@Module
public class ActivityModule {
@Provides
@PerActivity
MainInteractor provideMainInteractor () {
return new MainInteractor();
}
}
My TestActivityComponent
uses a 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. With the latter you can swap the component to a test component within 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 see this activity uses a MainFragment
. In onCreate()
of the fragment the component is injected:
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 swap the ActivityComponent
with the 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"))));
}
}
The Activity test runs but the wrong Component
is used. This is because activities and fragments onCreate()
is run before the component is swapped.
As you can see I have an commented old approach were I bind an ApplicationComponent
to the application class. This works because I can build the dependency before starting the activity. But now with the ActivityComponent
I have to launch the activity before initializing the injector. Because otherwise I could not set
mActivity.setActivityComponent(mTestActivityComponent);
because mActivity
would be null if would launch the activity after the initialization of the injector. (See MainActivityTest
)
So how could I intercept the MainActivity
and the MainFragment
to use the TestActivityComponent
?
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 setde.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:
In
swapActivityGraph()
I create an alternative TestActivityGraph for the Activity before(!) the Activity is created when running the test. Then we have to create aTestFragmentComponent
:This component lives in a Fragment-scope. It has a module:
The original
FragmentModule
looks like that:You see I use a
MainInteractor
and aFakeMainInteractor
. They both look like that:Now we use a self-defined
FragmentTestRule
for testing the Fragment independent from the Activity which contains it in production:That
TestActivity
is very simple: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
:The second trick is to use the holder to register the component before the fragment is even created. Then we launch the
TestActivity
with ourFragmentTestRule
. Now comes the third trick which is timing-dependent and does not always run correctly. Directly after launching the activity we get theFragment
instance by asking theFragmentTestRule
. Then we swap the component, using theTestFragmentComponentHolder
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 inonViewCreated()
. Because then we don't inject the component to early becauseonCreate()
andonCreateView()
are called before. So here is ourMainFragment
:And all the steps (second to forth trick) which I described before can be found in the
@Before
annotatedsetUp()
-Method in thisMainFragmentTest
class: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!