The docs are quite good when it comes to testing Fragments, but there's no info on how to test an Activity that uses ActivityResult.
How should we override activityResultRegistry in Activity tests?
The docs are quite good when it comes to testing Fragments, but there's no info on how to test an Activity that uses ActivityResult.
How should we override activityResultRegistry in Activity tests?
I wasn't reading the docs as precisely as I should have.
Note: Any mechanism that allows you to inject a separate
ActivityResultRegistryin tests is enough to enable testing your activity result calls.
Emphasis on the word inject.
I'm using Koin in my project so I decided to use the Scopes api to create an Activity Scoped instance of ActivityResultRegistry, which I injected into my registerForActivityResult-call.
val activityScopeModule = module {
scope<MyActivity> {
scoped { get<ComponentActivity>().activityResultRegistry }
}
}
class MyActivity: AppCompatActivity() {
private val requestPermLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
get<ActivityResultRegistry>() // Koin injection
) { granted ->
// handle
}
}
By using DI, injecting my custom test instance of ActivityResultRegistry in tests became very easy.
Useful blog post on the topic (uses Hilt to achieve the same task): https://blog.stylingandroid.com/activity-result-contract-outside-the-activity/
Blog post about Koin Scopes API: https://proandroiddev.com/understanding-android-scopes-with-koin-cfe6b60ca579
Write your contract in a separate file so you can easily test contracts and provide your own ActivityResultRegistry at runtime to fake expected results. It's bad practice to actually call a real contract to test from activity. One of the core purposes of designing contracts was to decouple activity code from onActicityResults
class ImageContract(registry: ActivityResultRegistry) {
private val contractUriResult : MutableLiveData<Uri> = MutableLiveData(null)
private val getPermission = registry.register(REGISTRY_KEY, ActivityResultContracts.GetContent()) { uri ->
contractUriResult.value = uri
}
fun getImageFromGallery(): LiveData<Uri> {
getPermission.launch("image/*")
return contractUriResult
}
companion object {
private const val REGISTRY_KEY = "Image Picker"
}
}
In your activity
ImageContractHandler(activityResultRegistry).getImageFromGallery().observe(this, {
it?.let { u ->
backgroundImageView.setImageURI(u)
}
})
In your Tests
@Test
fun activityResultTest() {
// Create an expected result URI
val testUrl = "file//dummy_file.test"
val expectedResult = Uri.parse(testUrl)
// Create the test ActivityResultRegistry
val testRegistry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, expectedResult)
}
}
val uri = ImageContractHandler(testRegistry).getImageFromGallery().getOrAwaitValue()
assert(uri == expectedResult)
}
For listening LiveData on the same thread in Tests a famous livedata test extension
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}