高度なコルーチンのテストにおけるKotlin Playgroundの応用


　 テストはソフトウェア開発において極めて要要な要素とされ、テストされたコードは保守性が向上します。また、これによりテストはしばしば事実上のドキュメントとしての役割も果たします。これに関連して、読者がKotlinを既に使用した経験を有する場合、おそらくは数多くのテストコードを作成したことであります。しかしながら、Kotlin Playgroundを活用してこれらのテストを実行・検証した経験を有するかは疑問であります。

![z4643068441154_b090d9371a2d8771e4e6f01105ccddba.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92345/ae08b769-c35e-48e8-d041-4fcef2e44c48.jpeg)

                  高度なコルーチンのテストにおけKotlin Playgroundの応用

　本記事は、Kotlin Playgroundを通じて高度なコルーチンを用いたコードのテスト手法について詳細に探究します。さらに、非同期処理を簡素化し効率的に取り扱う上で重要な役割を果たす高度なコルーチンの概念についても論じます。

1.「Kotlin Playground」とは何か
　素早く実現・確認する必要性を考える際、Android Studioや他のコードエディタを起動するのは時間がかかる手段が考えられるます。これは「Kotlin Playground」の使用目的です。具体的には、Kotlin PlaygroundはKotlinコードの実行環境を提供し、最大の特徴はその実行がウェブブラウザ上で行われる点であります。JetBrainsによって開発および維持されており、そのアクセスはplay.kotlinlang.orgを通じて行えます。

　Kotlin Playgroundには、標準的なKotlinライブラリ（例: コレクション、リフレクション、コルーチンなど）が組み込まれています。ただし、新たなライブラリの導入はサポート対象外である。言い換えれば、この環境は標準的なKotlinライブラリに依存するコードのプロトタイピングやテストに適しています。

　Kotlin Playgroundの際立った特徴の一つは、作成したコードを簡便に他者と共有できる点であります。URLをコピーして共有するか、「コードを共有」ボタンを活用して埋め込み可能なコードのバージョンを取得することができます。また、この環境は隠れた機能も備えており、Ctrlキー（Windows）またはCommandキー（Mac）を押しながらクリックすると、さらなるカーソルが生成されます。この機能は複数行のコードを同時に編集する際に特に便利です。

　2022年5月以降、モバイルデバイスでもKotlin Playgroundがサポートされるようになり、コンピュータを使用せずにKotlinコードを実行できるようになりました。また、特定のKotlinバージョンの選択やコンパイラの設定、プログラム引数の指定が可能なアクションツールバーも追加されています。

2.　Playgroundとの手順の説明
　Kotlin Playgroundでコードを実行するためには、mainメソッドの定義が必要であります。これを怠ると、「プロジェクトにメインメソッドが見つかりません」というエラーが発生します。本チュートリアルでは、Kotlin v1.7.21およびJVMコンパイラを使用します。まず、以下のimport文をファイルの先頭に必ず追加する必要があります。

kotlin.test.* のimport
kotlinx.coroutines.* のimport

　これらの設定により、以下のようなPlayground環境が構築されます。

![z4643069476120_d5031c3f87abfe4a7e6a0014dfaa37f5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92345/c58866aa-72ec-344e-0aff-14e5d25e07dc.jpeg)

「実行」ボタンを紫色で示されたボタンをクリックすることで、コードがコンパイルされ、エラーが表示されないことが期待されます。

さて、本研究では、テストの実施に移行します。我々はまず、通常のサスペンド関数のテスト方法について考察します。その後、テストディスパッチャーの切り替えおよび挿入方法、起動および非同期操作のテスト、ジョブおよびスーパーバイザージョブ、さらにはフローのテスト方法について論じます。

2.1.     通常のサスペンド機能のテスト

　まず初めに、サスペンド関数を定義し、その機能をテストするための適切なテストケースを準備します。ここでは、関数が有効な結果を返すことが期待されます。

suspend fun isEnabled(): Boolean {
    delay(1_000)
    return true
}

　次に、これらのテストケースをmain関数内に記述します。ここで、assertTrueはkotlin.testライブラリによって提供されるアサーションメソッドであり、与えられた条件が真であることを検証します。それ以外の場合は、例外が投げられることになります。

fun main() {
    runBlocking {
        val result = isEnabled()
          assertTrue(result)
    }
}


　このコードを実行すると、全てのテストが合格するため、出力は得られないでしょう。しかし、isEnabledの戻り値を偽に変更すると、次のようなエラーが報告されます。

Exception in thread "main" java.lang.AssertionError: Expected value to be true.

　また、必要に応じてカスタムメッセージをassertTrueに付加することもできます。
assertTrue(result, "result should be true but wasn't")

これにより、以下のような出力が得られます。

Exception in thread "DefaultDispatcher-worker-1 @coroutine#1" java.lang.Assertion


2.2     テストディスパッチャーの切り替えと挿入
　コード内でディスパッチャーをハードコーディングすることは推奨されないプラクティスです。むしろ、クラス内でディスパッチャーをパラメータとして受け入れるような設計が望ましいです。

　次のコードをご覧ください。

class Database {    
    private val scope = CoroutineScope(Dispatchers.IO)


    fun saveToDisk() {
        scope.launch {
            ...
          }
    }
}

ここでのテストはを伴いのはのはですから、簡素化するために以下のような変更が加えられます。

class Database(private val scope: CoroutineScope) {
    fun saveToDisk() {
        scope.launch {
            ...
          }
    }
}

これにより、テスト内でスコープを適切に挿入することができます。

2.3.　起動と非同期操作のテスト
launchおよびasyncは、特にAndroid開発においては、Composeフレームワークで広く使用される機能の一つです。こうした機能をどのようにテストすべきかを考えてみましょう。

まず最初に、状態を管理する簡単な関数を定義し、アクティビティやフラグメントのスコープがない状況においても、サスペンド関数を呼び出す方法を検討します。コルーチンを使用することで、メインスレッドのブロックを回避しつつ、バックエンドへのデータ保存を可能にします。

private val scope = CoroutineScope(Dispatchers.IO)

fun saveState() {
    scope.launch {
        // save state to backend, disk, ...
    }
}

データベースやサーバーの不在を勘案する際、操作が行われたかの如く変数を生成することが検討されます。

private val state: Any? = null

fun saveState() {
    scope.launch {
        state = "application state"
    }
}

実証可能な実体が具現されたため、テストケースを記述することとします。

fun main() {
    runBlocking {
        saveState()
          assertNotNull(state)
    }
}


これにより適正な機能が期待されるが、該当コードを実行した際、null状態のエラーが発生します。このコードの課題は、saveState関数を呼び出してはいるものの、その実行を待機せずに結果を検証しようとしている点にあります。

問題を解決するためには、saveState関数を呼び出す前に、微少な遅延を導入することであります。






fun main() {
    runBlocking {
        saveState()
          delay(100)
          assertNotNull(state)
    }
}

これにより、saveState関数を実行する適当な時間が確保されます。ただし、コードをテストするにあたり遅延を使用するのは最善の方法とは言えません。なぜなら、なぜ200ミリ秒ではなく正確に100ミリ秒を選択したのか、またコードの実行に100ミリ秒を超える時間がかかる場合の取り扱いは如何であるかについて考慮すべきであるからであります。したがって、このような行為は避けるべきであります。この記事の後半で、より優れた方法による試験手法について論じます。

非同期の試験もまた同様の手続きを要します。従って、saveState関数を非同期に適応することを検討します。

fun saveState() {
    scope.launch {
        async { state = "application state" }.await()
    }
}

fun main() {
    runBlocking {
        saveState()
      delay(100)
          assertNotNull(state)
    }
}



　続いて、このコードを実行することで、期待通りの動作が確認されるであります。

2.4.　テストジョブ／スーパーバイザージョブのテスト

　以下において、ジョブのテスト手法について検討いたします。GlobalScopeを用いる場合、コードの交換又はモック化が難しく、従ってコードのテストは著しく困難です。また、GlobalScopeの使用はキャンセル不能であり、このため基本的にジョブの制御は失われます。これに対して、必要に応じて制御可能なテストのためのカスタムスコープを定義いたします。

private val scope = CoroutineScope(Dispatchers.Default)

ジョブの追跡に用いる変数を定義し、起動の結果を当該変数に割り当てるようにsaveState関数を変更可能です。

private var job: Job? = null
private var state: Any? = null

fun saveState() {
    job = scope.launch {
        println("application state")
    }
}

この際、main関数内において、saveState関数をテストすることが可能となります。

fun main() {
    runBlocking {
        saveState()
          job?.join()
          assertNotNull(state)
    }
}

これを実行した場合、エラーは生じないものと考えられます。

「なぜjoinを用いる必要があるのか」と疑問が生じる可能性が考えられます。実際には、Launchを呼び出しても、コードの実行はブロックされません。そのため、main関数が終了しないようにするには、joinを利用する必要があります。

ジョブをテストする方法が把握できましたので、次にはSupervisorJobをテストする方法を探究いたしましょう。

SupervisorJobはジョブに類似しており、その子要素は相互に独立して失敗する可能性がある点が異なります。初めに、スコープを変更してSupervisorJobを組み込んでみましょう。

val scope = CoroutineScope(SupervisorJob())

次に、メイン関数において別の起動でエラーをスローする処理を追加いたします。

fun main() {
    runBlocking {
        scope.launch { throw error("launch1") }.join()
      saveState().join()
          assertNotNull(state)
    }
}


これを実行すると、出力にエラーが表示されるはずです。確かに、SupervisorJobはこのような状況を防ぐべきであると思われますが、より詳細に分析すると、実際にはエラーの発生自体は阻止されていますが、その内容がログに記録されることは妨げられていないことが分かります。アサーションの下にprintlnステートメントを追加すれば、実際にその内容が出力されることが確認できます。従って、初回の起動時にエラーが発生したとしても、二度目の起動では実行が可能であることが示されます。

2.5.  フローのテスト
フローをテストするには、最初に新たなインポートを加えます。

import kotlinx.coroutines.flow.*

続いて、observeDataという関数を生成し、それによってフローが返されるようにします。ここで、mainメソッド内でassertEquals関数を運用し、期待値と実際の値を照合することが可能です。

fun observeData(): Flow<String> {
    return flowOf("a", "b", "c")
}


3. コルーチンの最適な慣行

コルーチンの最適な慣行に従うためには、以下のコルーチンのベストプラクティスに準拠いただきますようお願い申し上げます。

ディスパッチャーのクラスへの組み込み: クラスにおいてディスパッチャーをハードコーディングすることは避けるべきです。この要素をインジェクションすることで、テストの容易性が向上し、交換可能性が保たれます。
GlobalScopeの避け方: GlobalScopeを使用することは、テストの困難さを増すため、その使用は慎重に考えるべきです。加えて、ジョブのライフサイクルを適切に制御することも難しくなる可能性があります。

4.　結論 
　本記事において、Kotlinプレイグラウンドを適用したコルーチンの試験手法を探究いたしました。また、CoroutineContextやFlowといったコルーチンの諸概念についても考察を行いました。最終的には、試験を円滑に行うための諸戦略に関する論考を行うに至りました。
　今般、学識の進境は読者の一層の探求に委ねられます。適材適所の練習が知の獲得における至上の方法であることを肝に銘じつつ、また次回の機会にお目にかかることを楽しみにしております。
テストで Kotlin Playground を使用できることは、Kotlin に切り替えることの大きな利点の 1 つです。 Kotlin とその将来性に興味がある場合は、次のリンクをチェックしてください。

https://miichisoft.com/kotlin-java-main-difference-how-to-choose/