Как протестировать действие, использующее API ActivityResult?

#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
}