Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save shekibobo/7e5e895bd502b5796e47dd87a0a89e27 to your computer and use it in GitHub Desktop.
Save shekibobo/7e5e895bd502b5796e47dd87a0a89e27 to your computer and use it in GitHub Desktop.
An ActivityScenario that allows you to use Dagger Android's automatic, lifecycle based injection without making your Application class `open`, or overriding it in tests.
package com.pixite.pigment.testing
import android.app.Activity
import android.app.Instrumentation
import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import java.io.Closeable
/**
* Creates an [InjectableActivityScenario] that uses the supplied injections.
*
* ```
* injectableActivityScenario<MyActivity> {
* injectActivity { // this: MyActivity
* dependency1 = myMockDependency1
* dependency2 = myMockDependency2
* }
* injectFragment<MyFragment> { // this: MyFragment
* dependency1 = myMockDependency1
* }
* }
* ```
*/
inline fun <reified T : Activity> injectableActivityScenario(injector: InjectableActivityScenario<T>.() -> Unit) =
InjectableActivityScenario(T::class.java).apply {
this.injector()
}
/**
* Adds an injector to the target [InjectableActivityScenario] for the given [Activity].
*/
inline fun <reified A : Activity> InjectableActivityScenario<*>.injectActivity(noinline injector: A.() -> Unit) {
injectActivity(A::class.java, injector)
}
/**
* Adds an injector to the target [InjectableActivityScenario] for the given [Fragment].
*/
inline fun <reified F : Fragment> InjectableActivityScenario<*>.injectFragment(noinline injector: F.() -> Unit) {
injectFragment(F::class.java, injector)
}
/**
* InjectableActivityScenario is an extension and wrapper of [ActivityScenario] which allows you to easily inject
* any [Activity]s and [Fragment]s that need to be loaded from the launch Activity.
*
* ```
* val activityScenario = InjectableActivityScenario(MyActivity::class.java)
* activityScenario.injectActivity { // this: MyActivity
* dependency1 = myMockDependency1
* dependency2 = myMockDependency2
* }
* activityScenario.injectFragment(MyFragment::class.java) { // this: MyFragment
* dependency1 = myMockDependency1
* }
* ```
*/
class InjectableActivityScenario<T : Activity>(private val activityClass: Class<T>) : AutoCloseable, Closeable {
private var delegate: ActivityScenario<T>? = null
private val activityInjectors = mutableListOf<ActivityInjector<out Activity>>()
private val fragmentInjectors = mutableListOf<FragmentInjector<out Fragment>>()
fun launch(startIntent: Intent? = null): InjectableActivityScenario<T> {
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(activityLifecycleObserver)
delegate = if (startIntent != null) {
ActivityScenario.launch(startIntent)
} else {
ActivityScenario.launch(activityClass)
}
return this
}
override fun close() {
delegate?.close()
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(activityLifecycleObserver)
}
fun moveToState(newState: Lifecycle.State): InjectableActivityScenario<T> {
val d =
delegate ?: throw IllegalStateException("Cannot move to state $newState since the activity hasn't been launched.")
d.moveToState(newState)
return this
}
fun recreate(): InjectableActivityScenario<T> {
val d = delegate ?: throw IllegalStateException("Cannot recreate the activity since it hasn't been launched.")
d.recreate()
return this
}
fun onActivity(action: (T) -> Unit): InjectableActivityScenario<T> {
val d = delegate ?: throw IllegalStateException("Cannot run onActivity since the activity hasn't been launched.")
d.onActivity(action)
return this
}
fun runOnMainThread(action: (T) -> Unit) {
val d = delegate ?: throw IllegalStateException("Cannot run onActivity since the activity hasn't been launched.")
d.onActivity(action)
}
fun getResult(): Instrumentation.ActivityResult =
delegate?.result ?: throw IllegalStateException("Cannot get result since activity hasn't been launched.")
/**
* Injects the target Activity using the supplied [injector].
*
* ```
* activityTestRule.addActivityInjector {
* // this is the target Activity
* dependency = fakeDependency
* }
* ```
*/
fun injectActivity(injector: T.() -> Unit) {
activityInjectors.add(ActivityInjector(activityClass, injector))
}
fun <A : Activity> injectActivity(activityClass: Class<A>, injector: A.() -> Unit) {
activityInjectors.add(ActivityInjector(activityClass, injector))
}
fun <F : Fragment> injectFragment(fragmentClass: Class<F>, injector: F.() -> Unit) {
fragmentInjectors.add(FragmentInjector(fragmentClass, injector))
}
fun <F : Fragment> injectFragment(fragment: F, injector: F.() -> Unit) {
fragmentInjectors.add(
FragmentInjector(
fragment::class.java,
injector
)
)
}
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 FragmentInjector<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
}
}
private val fragmentCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentPreAttached(fm: FragmentManager, f: Fragment, context: Context) {
fragmentInjectors.forEach {
if (it.inject(f)) return
}
}
}
private val activityLifecycleObserver = object : ActivityLifecycleCallback {
override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) {
when (stage) {
Stage.PRE_ON_CREATE -> {
if (activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentCallbacks, true)
}
activityInjectors.forEach { if (it.inject(activity)) return }
}
Stage.DESTROYED -> {
if (activity is FragmentActivity) {
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentCallbacks)
}
}
else -> {
// no op
}
}
}
}
}
class MyActivityTest {
@get:Rule val instantExecutorRule = InstantTaskExecutorRule()
private lateinit var activityScenario: InjectableActivityScenario<MyActivity>
private lateinit var sharedViewModel: MySharedViewModel
private lateinit var homeViewModel: HomeFragmentViewModel
private lateinit var detailViewModel: DetailViewModel
private lateinit var fakeNavigator: FakeNavigator
@Before fun setup() {
sharedViewModel = mock {
// ...
}
homeViewModel = mock {
// ...
}
detailViewModel = mock {
// ...
}
fakeNavigator = FakeNavigator()
activityScenario = injectableActivityScenario<MyActivity> {
injectActivity {
viewModelFactory = viewModelFactoryFor(sharedViewModel)
navigator = fakeNavigator
}
injectFragment(HomeViewModel::class.java) {
viewModelFactory = viewModelFactoryFor(homeViewModel)
}
injectFragment(DetailViewModel::class.java) {
viewModelFactory = viewModelFactoryFor(detailViewModel)
}
}.launch()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment