Last active
December 6, 2021 12:07
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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