1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

男坂Advent Calendar 2020

Day 24

AndroidのUIテストで、非同期処理が完了するまで待機する(Coroutine編)

Last updated at Posted at 2020-12-23

AndroidのUIテストの話。Espresso前提。

非同期処理にCoroutineを使っている場合に、sleepなしで処理完了まで待機する方法を説明する。

Rxと違ってプロダクトコードの修正も必要。テスト実行時は、DispatchersのIO/Defaultをテスト用のものにすり替えられるようにする1。そして、テスト実行時のDispatchersとIdlingResourceと紐付ける。

Step1. CoroutineのDispatchersをすり替えられるようにする

以下のようなクラスを作る。元ネタ。

CoroutinePlugin.kt
object CoroutinePlugin {

    private val defaultIoDispatcher: CoroutineContext = Dispatchers.IO
    val ioDispatcher: CoroutineContext
        get() = ioDispatcherHandler?.invoke(
                defaultIoDispatcher
        ) ?: defaultIoDispatcher

    @set:VisibleForTesting
    var ioDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null

    private val defaultComputationDispatcher: CoroutineContext = Dispatchers.Default
    val defaultDispatcher: CoroutineContext
        get() = computationDispatcherHandler?.invoke(
                defaultComputationDispatcher
        )
                ?: defaultComputationDispatcher

    @set:VisibleForTesting
    var computationDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null

    private val defaultMainDispatcher: CoroutineContext = Dispatchers.Main
    val mainDispatcher: CoroutineContext
        get() = mainDispatcherHandler?.invoke(
                defaultMainDispatcher
        ) ?: defaultMainDispatcher

    @set:VisibleForTesting
    var mainDispatcherHandler: ((CoroutineContext) -> CoroutineContext)? = null

    @VisibleForTesting
    @JvmStatic
    fun reset() {
        ioDispatcherHandler = null
        computationDispatcherHandler = null
        mainDispatcherHandler = null
    }
}

プロダクトコードからは、Dispatchers.IOやDefaultを直接使うのではなく、作ったCoroutinePlugin#ioDispatcherやdefaultDispatcherを参照する。

// withContext(Dispatchers.IO) { ... }
withContext(CoroutinePlugin.ioDispatcher) { ... }

Step2. Coroutine処理待機用のTestRuleを作る

(Kotlin 1.4向けの記述なので、1.3系だとコンパイルエラーになる)

class CoroutineTestRule : TestRule {
    override fun apply(base: Statement, description: Description?): Statement = object : Statement() {
        override fun evaluate() {
            // IdlingThreadPoolExecutorないときはbuild.gradleに以下を記述する。
            // androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:3.3.0"
            val executor = IdlingThreadPoolExecutor(
                    "Coroutine",
                    5,
                    10,
                    5L,
                    TimeUnit.SECONDS,
                    LinkedBlockingQueue()
            ) { r -> Thread(r) }

            CoroutinePlugin.ioDispatcherHandler =  { executor.asCoroutineDispatcher() }
            CoroutinePlugin.computationDispatcherHandler =  { executor.asCoroutineDispatcher() }

            try {
                base.evaluate() // この中でBefore/Test/Afterの処理が実行される
            } finally {
                CoroutinePlugin.reset()
                executor.shutdownNow()
            }
        }
    }
}
  • IdlingThreadPoolExecutorを作る
    • EspressoのIdlingResourceとJavaのExecutorServiceの両方を実装しているクラス
    • IdlingRegistryのregister/unrgesiterは、内部で実行してくれる
  • executorからCoroutineDispatcherを作り、CoroutinePluginのデフォルトのCoroutineDispatcherを差し替える

このTestRuleを使うと、Coroutineの非同期処理の完了までEspressoが待機するようになる。

Step3. UIテストコードの記述

TestRuleをテストクラスに適用する。以下、例。

@LargeTest
@RunWith(AndroidJUnit4::class)
class FooTests {
    @get:Rule
    val coroutineTestRule = CoroutineTestRule()
    ...
}
  1. Coroutineでもメインスレッドで実行する処理は、何も対処しなくてもEspressoが待機してくれるはず。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?