#android #android-activity #android-testing #android-instrumentation #registerforactivityresult
#Android #android-активность #android-тестирование #android-инструментарий #registerforactivityresult
Вопрос:
Документы довольно хороши, когда дело доходит до тестирования фрагментов, но нет информации о том, как протестировать действие, использующее ActivityResult.
Как мы должны переопределять activityResultRegistry
тесты активности?
Ответ №1:
Я не читал документы так точно, как следовало бы.
Примечание: любого механизма, который позволяет вводить отдельные
ActivityResultRegistry
тесты in, достаточно, чтобы включить тестирование ваших вызовов результатов activity.
Акцент на слове inject.
Я использую Koin в своем проекте, поэтому я решил использовать Scopes api для создания экземпляра Activity Scoped ActivityResultRegistry
, который я ввел в свой 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
}
}
С помощью DI внедрение моего пользовательского тестового экземпляра ActivityResultRegistry
in tests стало очень простым.
Полезный пост в блоге по теме (использует Hilt для решения той же задачи): https://blog.stylingandroid.com/activity-result-contract-outside-the-activity /
Сообщение в блоге об API Koin Scopes: https://proandroiddev.com/understanding-android-scopes-with-koin-cfe6b60ca579
Ответ №2:
Запишите свой контракт в отдельный файл, чтобы вы могли легко тестировать контракты и предоставлять свою собственную ActivityResultRegistry во время выполнения, чтобы подделать ожидаемые результаты. На самом деле вызывать реальный контракт для тестирования из activity — плохая практика. Одной из основных целей разработки контрактов было отделение кода activity от 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"
}
}
В вашей деятельности
ImageContractHandler(activityResultRegistry).getImageFromGallery().observe(this, {
it?.let { u ->
backgroundImageView.setImageURI(u)
}
})
В ваших тестах
@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)
}
Для прослушивания LiveData в том же потоке в тестах используется известное расширение теста livedata
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
}