Skip to content

Instantly share code, notes, and snippets.

@rharter
Last active December 6, 2021 12:07
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rharter/355064cb64baff3830898dca0414ea9b to your computer and use it in GitHub Desktop.
Save rharter/355064cb64baff3830898dca0414ea9b to your computer and use it in GitHub Desktop.
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