I have built up an example application (Yes, it is really just an example and doesn’t make much sense but is good for understanding Android clean architecture and dependency injection in Dagger 2). My
code is available on github.(Outdated. See this post) The example app just let’s you type in a name in an EditText
and if you press the Button you see a message "Hello YourName"
I have three different Components: ApplicationComponent
, ActivityComponent
and FragmentComponent
. FragmentComponent
contains three modules:
InteractorModule
provides a MainInteractor
.
@Module
public class InteractorModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new MainInteractor();
}
}
In my Activity-UnitTest I want to fake this MainInteractor
. This Interactor just has a method public Person createPerson(String name)
which can create a Person object. The FakeMainInteractor
has the same method but always creates a Person object with the name „Fake Person“, indepent of the parameter you have passed.
public class FakeMainInteractor {
public Person createPerson(final String name) {
return new Person("Fake Person");
}
}
I already createt TestComponents for evey Component I described above. And In TestFragmentComponent
I swapped InteractorModule
with TestInteractorModule
.
@PerFragment
@Component(dependencies = TestApplicationComponent.class, modules = {ActivityModule.class, FragmentModule.class, TestInteractorModule.class})
public interface TestFragmentComponent {
void inject(MainFragment mainFragment);
void inject(MainActivity mainActivity);
}
This example is running well in a non-test context. In the MainActivity
I have a method called initializeInjector()
where I build the FragmentComponent
. And onCreate()
calls onActivitySetup()
which
calls initializeInjector()
and inject()
.
public class MainActivity extends BaseActivity implements MainFragment.OnFragmentInteractionListener,
HasComponent<FragmentComponent> {
private FragmentComponent fragmentComponent;
private Fragment currentFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
currentFragment = new MainFragment();
addFragment(R.id.fragmentContainer, currentFragment);
}
}
private void initializeInjector() {
this.fragmentComponent = DaggerFragmentComponent.builder()
.applicationComponent(getApplicationComponent())
.activityModule(getActivityModule())
.fragmentModule(getFragmentModule())
.build();
}
@Override
protected void onActivitySetup() {
this.initializeInjector();
fragmentComponent.inject(this);
}
@Override
public void onFragmentInteraction(final Uri uri) {
}
@Override public FragmentComponent getComponent() {
return fragmentComponent;
}
public FragmentModule getFragmentModule() {
return new FragmentModule(currentFragment);
}
}
This works fine. And my MainActivityTest
also works fine. It tests the typing in of the name and the result of the following button click. But the TextView
shows „Hello John“.
public class MainActivityTest implements HasComponent<TestFragmentComponent> {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule(MainActivity.class, true, true);
private MainActivity mActivity;
private TestFragmentComponent mTestFragmentComponent;
@Before
public void setUp() throws Exception {
mActivity = mActivityRule.getActivity();
}
@Test
public void testMainFragmentLoaded() throws Exception {
mActivity = mActivityRule.getActivity();
assertTrue(mActivity.getCurrentFragment() instanceof MainFragment);
}
@Test
public void testOnClick() 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 TestFragmentComponent getComponent() {
return mTestFragmentComponent;
}
}
But as I told I want to use the FakeMainInteractor
which would print „Hello Fake Person“. But I don’t know how to build up the dependency graph within the Test. So in test mode I want another graph to be created, using the TestComponents and TestModules instead of the original Components and Modules. So how to do that? How to let the test use FakeMainInteractor
?
As I told, I know this example app doesn’t do anything useful. But I would like to understand Testing with Dagger 2. I already read this article. But it just shows how to make the TestComponents and the TestModules. It does not tell how to use a Test-Graph in the Unit Test. How to do that? Can someone provide some example code?
This is not a solution for me, because it uses and older version of Dagger 2 (I use version 2.7) and it does not describe how to wire the TestComponents.
After trying approach by @DavidRawson some of my classes changed their implementation:
public class MainActivityTest{
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule(MainActivity.class, true, true);
private MainActivity mActivity;
private TestApplicationComponent mTestApplicationComponent;
private TestFragmentComponent mTestFragmentComponent;
private void initializeInjector() {
mTestApplicationComponent = DaggerTestApplicationComponent.builder()
.applicationModule(new ApplicationModule(getApp()))
.build();
getApp().setApplicationComponent(mTestApplicationComponent);
mTestFragmentComponent = DaggerTestFragmentComponent.builder()
.testApplicationComponent(mTestApplicationComponent)
.activityModule(mActivity.getActivityModule())
.testInteractorModule(new TestInteractorModule())
.build();
mActivity.setFragmentComponent(mTestFragmentComponent);
mTestApplicationComponent.inject(this);
mTestFragmentComponent.inject(this);
}
public AndroidApplication getApp() {
return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
@Before
public void setUp() throws Exception {
mActivity = mActivityRule.getActivity();
initializeInjector();
}
@Test
public void testMainFragmentLoaded() throws Exception {
mActivity = mActivityRule.getActivity();
assertTrue(mActivity.getCurrentFragment() instanceof MainFragment);
}
@Test
public void testOnClick() 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"))));
}
}
MainActivity
owns the following new method:
@Override
public void setFragmentComponent(final FragmentComponent fragmentComponent) {
Log.w(TAG, "Only call this method to swap test doubles");
this.fragmentComponent = fragmentComponent;
}
AndroidApplication
owns:
public void setApplicationComponent(ApplicationComponent applicationComponent) {
Log.w(TAG, "Only call this method to swap test doubles");
this.applicationComponent = applicationComponent;
}
You can write a setter method in the Application
to override the root Component
Modify your current Application
class by adding this method:
public class AndroidApplication extends Application {
@VisibleForTesting
public void setApplicationComponent(ApplicationComponent applicationComponent) {
Log.w(TAG, "Only call this method to swap test doubles");
this.applicationComponent = applicationComponent;
}
}
now in your test setup method, you can swap the real root Component
with the fake one:
@Before
public void setUp() throws Exception {
TestApplicationComponent component =
DaggerTestApplicationComponent.builder()
.applicationModule(new TestApplicationModule()).build();
getApp().setComponent(component);
}
private AndroidApplication getApp() {
return (AndroidApplication) InstrumentationRegistry.getInstrumentation()
.getTargetContext().getApplicationContext();
}
If you are using dependent subcomponents, you will probably have to, again, write a method called setComponent
inside your BaseActivity
. Please note that adding public getters and setters can be, in general, bad OO design practice but this is currently the simplest solution for performing hermetic tests using Dagger 2. These methods are documented here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With