Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Test Rule that allows you to use Dagger Android's automatic lifecycle based injection without making your Application class `open`, or overriding it in tests.
class MyActivityTest {
@get:Rule val instantExecutorRule = InstantTaskExecutorRule()
@get:Rule val activityRule = makeInjectableActivityRule<MyActivity>(launchActivity = false)
private lateinit var sharedViewModel: MySharedViewModel
private lateinit var homeViewModel: HomeFragmentViewModel
private lateinit var detailViewModel: DetailViewModel
@Before fun setup() {
sharedViewModel = mock {
// ...
}
homeViewModel = mock {
// ...
}
detailViewModel = mock {
// ...
}
activityRule.addFragmentInjector(HomeFragment::class.java) { homeFragment ->
homeFragment.sharedViewModel = sharedViewModel
homeFragment.viewModel = homeViewModel
}
activityRule.addFragmentInjector(DetailFragment::class.java) { detailFragment ->
detailFragment.sharedViewModel = sharedViewModel
detailFragment.vieWModel = detailViewModel
}
activityRule.addActivityInjector { myActivity ->
myActivity.sharedViewModel = sharedViewModel
// other injected dependencies
}
activityRule.launchActivity(null)
}
}
package com.pixite.pigment.testing
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
/**
* Creates an InjectableActivityTestRule for the specified Activity type.
*/
inline fun <reified T : Activity> makeInjectableActivityRule(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true
) = InjectableActivityTestRule(
T::class.java,
initialTouchMode = initialTouchMode,
launchActivity = launchActivity
)
/**
* This rule provides functional testing of a single activity that uses Dagger Android's
* Activity and Fragment injection via lifecycle callbacks, without the need to override
* the Application class in tests.
*
* Adding Activity or Fragment injectors will allow injector code to be run when the Activity
* is created, or before the Fragment is attached.
*/
class InjectableActivityTestRule<T : Activity>(
private val activityClass: Class<T>,
targetPackage: String = InstrumentationRegistry.getInstrumentation().targetContext.packageName,
launchFlags: Int = Intent.FLAG_ACTIVITY_NEW_TASK,
initialTouchMode: Boolean = false,
launchActivity: Boolean = true
) : ActivityTestRule<T>(
activityClass,
targetPackage,
launchFlags,
initialTouchMode,
launchActivity
) {
private val activityInjectors = mutableListOf<ActivityInjector<out Activity>>()
private val fragmentInjectors = mutableListOf<FragmentInjection<out Fragment>>()
private val activityCallbacks = object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
if (activity is FragmentActivity) {
activity.supportFragmentManager
.registerFragmentLifecycleCallbacks(fragmentCallbacks, true)
}
activityInjectors.forEach { if (it.inject(activity)) return }
}
override fun onActivityDestroyed(activity: Activity?) {
if (activity is FragmentActivity) {
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentCallbacks)
}
}
override fun onActivityPaused(activity: Activity?) {}
override fun onActivityResumed(activity: Activity?) {}
override fun onActivityStarted(activity: Activity?) {}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
override fun onActivityStopped(activity: Activity?) {}
}
private val fragmentCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentPreAttached(fm: FragmentManager, f: Fragment, context: Context) {
fragmentInjectors.forEach {
if (it.inject(f)) return
}
}
}
/**
* Injects the target Activity using the supplied [injector].
*
* ```
* activityTestRule.addActivityInjector {
* // this is the target Activity
* dependency = fakeDependency
* }
* ```
*/
fun addActivityInjector(injector: T.() -> Unit) {
activityInjectors.add(ActivityInjector(activityClass, injector))
}
fun <A : Activity> addActivityInjector(activityClass: Class<A>, injector: A.() -> Unit) {
activityInjectors.add(ActivityInjector(activityClass, injector))
}
fun <F : Fragment> addFragmentInjector(fragmentClass: Class<F>, injector: F.() -> Unit) {
fragmentInjectors.add(FragmentInjection(fragmentClass, injector))
}
fun <F : Fragment> addFragmentInjector(fragment: F, injector: F.() -> Unit) {
fragmentInjectors.add(FragmentInjection(fragment::class.java, injector))
}
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
val application = InstrumentationRegistry.getInstrumentation().targetContext
.applicationContext as Application
application.registerActivityLifecycleCallbacks(activityCallbacks)
}
override fun afterActivityFinished() {
val application = InstrumentationRegistry.getInstrumentation().targetContext
.applicationContext as Application
application.unregisterActivityLifecycleCallbacks(activityCallbacks)
super.afterActivityFinished()
}
private class ActivityInjector<A : Activity>(
private val activityClass: Class<A>,
private val injector: A.() -> Unit
) {
fun inject(activity: Activity?): Boolean {
if (activityClass.isInstance(activity)) {
activityClass.cast(activity)!!.injector()
return true
}
return false
}
}
private class FragmentInjection<F : Fragment>(
private val fragmentClass: Class<F>,
private val injection: F.() -> Unit
) {
fun inject(fragment: Fragment?): Boolean {
if (fragmentClass.isInstance(fragment)) {
fragmentClass.cast(fragment)!!.injection()
return true
}
return false
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment