All Articles

Faking Dagger Injection in Instrumentation Tests

I’ve recently started a project with the sole purpose of demo-ing a modular clean architecture to my team. In this architecture, we have different modules per feature. For example, the demo project had a list fragment and a details fragment and each one lived in a separate module.

Dagger Android was also used to inject dependencies to both fragments, which means that I will usually have something like this:

class ListFragment: Fragment(R.layout.fragment_list) {
    fun onAttached(....) {
        AndroidSupportInjection.inject(this)
        super.onAttach(...)
    }
}

This meant that I had to figure out a way to make AndroidSupportInjection.inject work.

How does AndroidSupportInjection.inject work?

If we dig into the source code of this function, you will quickly see another private function named findHasAndroidInjectorForFragment. Examining this code tells us that Dagger looks into possible parents of the calling fragment to find out which one holds an AndroidInjector for it.

  private static HasAndroidInjector findHasAndroidInjectorForFragment(Fragment fragment) {
    Fragment parentFragment = fragment;
    while ((parentFragment = parentFragment.getParentFragment()) != null) {
      if (parentFragment instanceof HasAndroidInjector) {
        return (HasAndroidInjector) parentFragment;
      }
    }
    Activity activity = fragment.getActivity();
    if (activity instanceof HasAndroidInjector) {
      return (HasAndroidInjector) activity;
    }
    if (activity.getApplication() instanceof HasAndroidInjector) {
      return (HasAndroidInjector) activity.getApplication();
    }
    throw new IllegalArgumentException(
        String.format("No injector was found for %s", fragment.getClass().getCanonicalName()));
  }

This information is usually stored in another class: DispatchingAndroidInjector, if we look more into it we find that it stores this information in a Map.

// DispatchingAndroidInjector constructor
DispatchingAndroidInjector(
      Map<Class<?>, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithClassKeys,
      Map<String, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithStringKeys) {
    this.injectorFactories = merge(injectorFactoriesWithClassKeys, injectorFactoriesWithStringKeys);
  }

So when a fragment calls AndroidSupportInjection.inject, Dagger will look into the nearest parent that can provide AndroidInjector for this fragment and then uses this AndroidInjector to inject dependencies.

It’s important to bear in mind that Dagger could potentially have more logic than this, but that’s all we need know for setting up instrumentation tests.

Setting up our test

After looking into how the AndroidSupportInjection.inject behaves, let’s have a look at building an instrumentation test setup that will allow us to test any dagger injected fragment.

Creating a Host Activity

The logic of findHasAndroidInjectorForFragment tells us that the fragment under test needs to have a parent. This parent needs to implement HasAndroidInjector and provide a DispatchingAndroidInjector for dagger. So a starting point for this test setup would be implementing a TestActivity which would host the fragment under test.

class TestActivity : FragmentActivity(), HasAndroidInjector {
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

    override fun androidInjector(): AndroidInjector<Any> {
        return dispatchingAndroidInjector
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(
            FragmentContainerView(this).apply {
                layoutParams = FrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
                id = R.id.test_fragment_container
            }
        )
    }
}

Don’t forget to create an AndroidManifest.xml in androidTest/ folder and declare TestActivity.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.amryousef.sample.lib.ui.testing">

    <application>
        <activity android:theme="@style/Theme.MyApp" android:name="me.amryousef.lib.ui.test.TestActivity" />
    </application>
</manifest>

Next, we need to add some logic to pass in the type of fragment under test, logic to inject dependencies in this fragment and finally, logic to display the fragment and test it.

ActivityScenario, in androidx.test:core:1.0.0, is a simple tool we can use to launch and interact with an Activity in instrumentation. This means that our fragment test can start with:

val fragment = ListFragment()
val scenario = ActivityScenario.launch(TestActivity::class.java)
scenario.onActivity { activity ->
    activity.supportFragmentManager.beginTransaction()
      .replace(R.id.test_fragment_container, fragment)
      .commitAllowingStateLoss()
}

As it stands, this logic will throw an exception since dispatchingAndroidInjector in TestActivity hasn’t been initialised yet. However, since we have access to the activity instance and we understand what dagger expects, we can easily add this part.

Faking Dagger Injection

Let’s start by creating DispatchingAndroidInjector and for this we can use DispatchingAndroidInjector_Factory to build it.

DispatchingAndroidInjector_Factory constructor looks like

DispatchingAndroidInjector_Factory(
      Provider<Map<Class<?>, Provider<AndroidInjector.Factory<?>>>> injectorFactoriesWithClassKeysProvider,
      Provider<Map<String, Provider<AndroidInjector.Factory<?>>>> injectorFactoriesWithStringKeysProvider)

This might seem complicated but it’s actually pretty simple so let’s break it down.

The constructor takes two providers. Each provider provides a Map. Both provided maps has a provider of an AndroidInjector.Factory which dagger will use to create AndroidInjector.

Provider is just a simple interface with get method

public interface Provider<T> {

    /**
     * Provides a fully-constructed and injected instance of {@code T}.
     *
     * @throws RuntimeException if the injector encounters an error while
     *  providing an instance. For example, if an injectable member on
     *  {@code T} throws an exception, the injector may wrap the exception
     *  and throw it to the caller of {@code get()}. Callers should not try
     *  to handle such exceptions as the behaviour may vary across injector
     *  implementations and even different configurations of the same injector.
     */
    T get();
}

So for our TestActivity, we need Provider<SOME_KEY, Provider<AndroidInject.Factory<ListFragment>>>. The SOME_KEY here represents the keys dagger uses in its code which are Class<?> and String. We can start by building Provider<AndroidInject.Factory<ListFragment>>

val provider: Provider<AndroidInjector.Factory<*>> = Provider {
    AndroidInjector.Factory<ListFragment> {
        AndroidInjector<ListFragment> { instance ->
            instance.injector()
        }
    }
}

We can then create the two maps dagger is expecting:

val stringKeyMap = mutableMapOf(ListFragment::class.java.name to provider)
val classKeyMap = mutableMapOf(ListFragment::class.java to provider)

And finally create the DispatchingAndroidInjector:

val dispatchingAndroidInjector = DispatchingAndroidInjector_Factory<Any>(stringKeyMap, classKeyMap).get()

Tying it all together with the ActivityScenario it can look like:

val scenario = ActivityScenario.launch(TestActivity::class.java)
scenario.onActivity { activity ->
    val provider: Provider<AndroidInjector.Factory<*>> = Provider {
        AndroidInjector.Factory<ListFragment> {
            AndroidInjector<ListFragment> { instance ->
                instance.injector()
            }
        }
    }
    val stringKeyMap = mutableMapOf(ListFragment::class.java.name to provider)
    val classKeyMap = mutableMapOf(ListFragment::class.java to provider)
    activity.dispatchingAndroidInjector = DispatchingAndroidInjector_Factory<Any>(stringKeyMap, classKeyMap).get()
}
val fragment = ListFragment()
scenario.onActivity { activity ->
    activity.supportFragmentManager.beginTransaction()
      .replace(R.id.test_fragment_container, fragment)
      .commitAllowingStateLoss()
}

To make this logic more reusable we can:

  • Create ActivityScenarioWrapper
  • Split dagger injection setup into its own generic function
  • Split displaying the fragment logic into its own generic function

Which leads us to an implementation looking like this:

class ActivityScenarioWrapper {

    val scenario = ActivityScenario.launch(TestActivity::class.java)

    inline fun <reified F : Fragment> injectFragment(
        crossinline injector: F.() -> Unit
    ) {
        val provider: Provider<AndroidInjector.Factory<*>> = Provider {
            AndroidInjector.Factory<F> {
                AndroidInjector<F> { instance ->
                    instance.injector()
                }
            }
        }
        scenario.onActivity {
            it.dispatchingAndroidInjector = DispatchingAndroidInjector_Factory<Any>(
                { mutableMapOf(F::class.java to provider) },
                { mutableMapOf(F::class.java.canonicalName to provider) }
            ).get()
        }
    }

    fun displayFragment(fragment: Fragment) {
        scenario.onActivity {
            it.supportFragmentManager.beginTransaction()
                .replace(R.id.test_fragment_container, fragment)
                .commitAllowingStateLoss()
        }
    }
}

Writing Fragment Test

We can write a working fragment test. However, it looks verbose and cumbersome to write.

@RunWith(AndroidJUnit4::class)
class ListFragmentTest {

    @Test
    fun firstTest() {
        val scenarioWrapper = ActivityScenarioWrapper()
        scenarioWrapper.injectFragment<ListFragment> {
            // Perform Injection
        }
        scenarioWrapper.displayFragment(ListFragment())
        // Test Logic
    }

}

We can simplify all this by wrapping it in our fragmentTest function which we can reuse to setup tests for any dagger injected fragment.

inline fun <reified F : Fragment> fragmentTest(
    bundle: Bundle? = null,
    crossinline injector: (fragment: F) -> Unit,
    crossinline test: (scenario: ActivityScenario<TestActivity>) -> Unit
) {
    val scenarioWrapper = ActivityScenarioWrapper()
    scenarioWrapper.scenario.onActivity {
        activityScenario.injectFragment<F> {
            injector(this)
        }
    }
    scenarioWrapper.scenario.onActivity {
        val fragment = F::class.java.newInstance().apply {
            arguments = bundle
        }
        scenarioWrapper.displayFragment(fragment)
    }
    test(activityScenario.scenario)
    scenarioWrapper.scenario.close()
}

You can find the complete solution and examples of how I used this setup in my sample repo on Github. All the logic you’ve seen here is under lib-ui-test module.