Testing Dagger Fragments with FragmentScenario

Remco Mokveld
ProAndroidDev
Published in
3 min readApr 12, 2019

--

As can be seen from my previous blog posts I really like Dagger. With the recent arrival of the FragmentScenario, I thought I’d give it a try to test Fragments. However, I ran into a problem on how to use that with Dagger Android. In this article I am going to describe my findings and discuss the solution I came up with.

Lets take the following example which is a Fragment that has its button enabled based on a state in LiveData.

import dagger.android.support.DaggerFragmentclass MyFragment: DaggerFragment() {
@Inject lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.liveData.observe(this) { button.isEnabled = it }
}
}

In this article we will be looking at a way to test this using FragmentScenario. In a followup article, we will look at how the approach taken will help for use cases other than testing as well.

The Fragment above is very simple. When it’s ViewModel LiveData<Boolean> is true it should show an enabled button, else it should be disabled.

Using the new FragmentScenario we can test individual UI Fragments by callinglaunchFragmentInContainer<T>().

@RunWith(AndroidJUnit4::class)
class MyFragmentTest {
@Test
fun launchFragment() {
launchFragmentInContainer<MyFragment>()
}
}

When we try to run this test, it crashes. Even though, in production code, it works just fine. Let’s take a look at the crash.

java.lang.IllegalArgumentException: 
No injector was found for MyFragment
at AndroidSupportInjection
.findHasFragmentInjector(AndroidSupportInjection.java:92)
at AndroidSupportInjection
.inject(AndroidSupportInjection.java:57)
at DaggerFragment.onAttach(DaggerFragment.java:38)

In DaggerFragment.onAttach the library calls AndroidSupportInjection.inject(this) to inject the members. This method will walk through the parent Fragment tree to find a parent which can inject its dependencies. If it can’t find it in any of its parent Fragments it will try the Activity. If the Activity can’t inject it, it will try the Application. If that also fails it will throw the exception above.

When you think about what AndroidSupportInjection.inject(this) is actually supposed to do, you will realize that it’s contract is that after it is being called, all @Inject annotated fields and methods are injected.

This is something we can easily do ourselves in tests. Thus if we define our own instance of dagger.android.support.DaggerFragment which makes the AndroidSupportInjection call overridable we can create a test subclass which does manual injection.

So instead of:

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}

It does this:

override fun onAttach(context: Context) {
injectMembers()
super.onAttach(context)
}
protected open fun injectMembers() =
AndroidSupportInjection.inject(this)

Now when MyFragment extends this new DaggerFragment and is marked open. We can define class TestMyFragment: MyFragment() in our test sources which overrides injectMembers and instead of calling the super, which calls Dagger, it can call do it’s own dependency injection.

class TestMyFragment : MyFragment() {
override fun injectMembers() {
this.viewModel = testViewModel
}
companion object {
// static property can be set before launching the Fragment
// to a mock instance
lateinit var testViewModel: MyViewModel
}
}

Now when launching this TestMyFragment instead of MyFragment we can test all code in the latter without needing to do anything Dagger related.

@RunWith(AndroidJUnit4::class)
class MyFragmentTest {
@Test
fun buttonIsEnabled() {
TestMyFragment.testViewModel = MyViewModel().also
it.updateState(enabled = true)
}
launchFragmentInContainer<TestMyFragment>() onView(withId(R.id.button)).check(matches(isEnabled()))
}
@Test
fun buttonIsDisabled() {
TestMyFragment.testViewModel = MyViewModel().also
it.updateState(enabled = false)
}
launchFragmentInContainer<TestMyFragment>() onView(withId(R.id.button)).check(matches(not(isEnabled())))
}
}

As you can see these tests are now concise and it is super easy to verify the UI of a Fragment.

To see this in action, check out the sample project that was published as part of the presentation ‘Single Activity by Example’ I gave at Droidcon Italy.

That’s it for now, in a follow up article I will discuss how we can actually use this method overriding in production as well.

--

--