この記事は、KotlinMultiplatformでのMVIアーキテクチャパターンの適用に関するシリーズの最後です。前の2つのパート(パート1とパート2)では、MVIとは何かを思い出し、猫の画像をロードするための汎用Kittensモジュールを作成し、それをiOSおよびAndroidアプリケーションに統合しました。
このパートでは、Kittensモジュールについてユニットテストと統合テストについて説明します。Kotlin Multiplatformでのテストの現在の制限について学び、それらを克服する方法を理解し、さらにはそれらを有利に機能させる方法を理解します。
更新されたサンプルプロジェクトは、GitHubで入手できます。
プロローグ
テストがソフトウェア開発の重要なステップであることは間違いありません。もちろん、それはプロセスを遅くしますが、同時に:
- 手動でキャッチするのが難しいエッジケースをチェックできます。
- 新機能の追加、バグの修正、リファクタリングの際のリグレッションの可能性を減らします。
- コードを分解して構造化するように強制します。
一見、最後のポイントは時間がかかるので不利に思えるかもしれません。ただし、長期的にはコードが読みやすく有益になります。
「確かに、読み取りと書き込みに費やされる時間の比率は10対1をはるかに超えています。新しいコードを作成する取り組みの一環として、常に古いコードを読み取っています。... [したがって]読みやすくすることで、書きやすくなります。」--Robert C. Martin、「Clean Code:A Handbook of AgileSoftwareCraftsmanship」
Kotlin Multiplatformは、テスト機能を拡張します。このテクノロジーは、1つの重要な機能を追加します。各テストは、サポートされているすべてのプラットフォームで自動的に実行されます。たとえば、AndroidとiOSのみがサポートされている場合、テストの数を2倍にすることができます。また、ある時点で別のプラットフォームのサポートが追加されると、自動的にテストの対象になります。
コードの動作に違いがある可能性があるため、サポートされているすべてのプラットフォームでのテストは重要です。たとえば、Kotlin / Nativeには特別なメモリモデルがあり、Kotlin / JSでも予期しない結果が生じることがあります。
先に進む前に、KotlinMultiplatformでのテストの制限のいくつかに言及する価値があります。最大の問題は、Kotlin / NativeおよびKotlin / JS用のモックライブラリがないことです。これは大きなデメリットのように思えるかもしれませんが、個人的にはメリットだと思います。 Kotlin Multiplatformでのテストは、私にとって非常に困難でした。依存関係ごとにインターフェイスを作成し、それらのテスト実装(偽物)を作成する必要がありました。長い時間がかかりましたが、ある時点で、抽象化に時間を費やすことは、よりクリーンなコードにつながる投資であることに気付きました。
また、このコードへのその後の変更にかかる時間が短いことにも気づきました。何故ですか?クラスとその依存関係との相互作用は釘付け(モック)されていないためです。ほとんどの場合、テストの実装を更新するだけで十分です。モックを更新するために、すべてのテスト方法を深く掘り下げる必要はありません。その結果、標準のAndroid開発でもモックライブラリの使用をやめました。次の記事を読むことをお勧めします:PravinSonawaneによる「モッキングは実用的ではありません-偽物を使用してください」。
予定
Kittensモジュールに何があり、何をテストする必要 があるかを覚えておきましょう。
- KittenStoreはモジュールの主要コンポーネントです。そのKittenStoreImpl実装には、ほとんどのビジネスロジックが含まれています。これが最初にテストすることです。
- KittenComponentは、すべての内部コンポーネントのモジュールファサードおよび統合ポイントです。このコンポーネントについては、統合テストで説明します。
- KittenViewは、KittenComponentのUI依存関係を表すパブリックインターフェイスです。
- KittenDataSourceは、iOSおよびAndroid用のプラットフォーム固有の実装を持つ内部Webアクセスインターフェイスです。
モジュールの構造をよりよく理解するために、そのUML図を示し
ます。
- KittenStoreのテスト
- KittenStore.Parserのテスト実装の作成
- KittenStore.Networkのテスト実装の作成
- KittenStoreImplのユニットテストの作成
- KittenStore.Parserのテスト実装の作成
- KittenComponentのテスト
- KittenDataSourceのテスト実装の作成
- テストKittenView実装を構築する
- KittenComponentの統合テストの作成
- KittenDataSourceのテスト実装の作成
- テストの実行
- 結論
KittenStoreユニットテスト
KittenStoreインターフェースには、独自の実装クラスであるKittenStoreImplがあります。これが私たちがテストしようとしているものです。クラス自体で直接定義された2つの依存関係(内部インターフェイス)があります。それらのテスト実装を作成することから始めましょう。
KittenStore.Parserの実装をテストします
このコンポーネントは、ネットワーク要求を担当します。これはそのインターフェースがどのように見えるかです:
ネットワークインターフェイスのテスト実装を作成する前に、1つの重要な質問に答える必要があります。サーバーはどのデータを返すのでしょうか。答えは、サーバーが画像リンクのランダムなセットを、毎回異なるセットを返すということです。実際にはJSON形式が使用されますが、パーサー抽象化があるため、ユニットテストでは形式を気にしません。
実際の実装ではストリームを切り替えることができるため、サブスクライバーはKotlin / Nativeでフリーズできます。この動作をモデル化して、コードがすべてを正しく処理することを確認するのは素晴らしいことです。
したがって、ネットワークのテスト実装には次の機能が必要です。
- リクエストごとに、空でない異なる行のセットを返す必要があります。
- 応答形式は、ネットワークとパーサーで共通である必要があります。
- ネットワークエラーをシミュレートできる必要があります(おそらく応答なしで完了する必要があります)。
- 無効な応答形式をシミュレートできる必要があります(パーサーのエラーをチェックするため)。
- (ブートフェーズをテストするために)応答遅延をシミュレートできる必要があります。
- Kotlin / Nativeでフリーズ可能である必要があります(念のため)。
テストの実装自体は次のようになります。
TestKittenStoreNetworkには(実サーバーと同じように)文字列ストレージがあり、文字列を生成できます。リクエストごとに、現在の行リストが1行にエンコードされます。「images」プロパティがゼロの場合、たぶん終了するだけで、エラーと見なされます。TestScheduler
も使用しました。このスケジューラには1つの重要な機能があります。それは、すべての着信タスクをフリーズすることです。したがって、TestSchedulerと組み合わせて使用されるobserveOnオペレーターは、実際の生活と同じように、ダウンストリームと、それを通過するすべてのデータをフリーズします。しかし同時に、マルチスレッドは関与しないため、テストが簡素化され、信頼性が向上します。
さらに、TestSchedulerには、ネットワークの待ち時間をシミュレートできる特別な「手動処理」モードがあります。
KittenStore.Parserの実装をテストします
このコンポーネントは、サーバーからの応答の解析を担当します。そのインターフェースは次のとおりです。
したがって、Webからダウンロードしたものはすべて、リンクのリストに変換する必要があります。私たちのネットワークは、セミコロン(;)区切り文字を使用して文字列を連結するだけなので、ここでは同じ形式を使用します。
テストの実装は次のとおりです。
ネットワークと同様に、TestSchedulerを使用してサブスクライバーをフリーズし、Kotlin /ネイティブメモリモデルとの互換性を確認します。入力文字列が空の場合、応答処理エラーがシミュレートされます。
KittenStoreImplのユニットテスト
これで、すべての依存関係のテスト実装ができました。ユニットテストの時間です。すべてのユニットテストはリポジトリにあります。ここでは、初期化といくつかのテストのみを示します。
最初のステップは、テスト実装のインスタンスを作成することです。
KittenStoreImplはmainSchedulerを使用するため、次のステップはそれをオーバーライドすることです。
これで、いくつかのテストを実行できます KittenStoreImplは、作成後すぐにイメージをロードする必要があります。つまり、ネットワークリクエストを実行し、その応答を処理して、状態を新しい結果で更新する必要があります。
我々のしたこと:
- ネットワーク上で生成された画像。
- KittenStoreImplの新しいインスタンスを作成しました。
- 状態に文字列の正しいリストが含まれていることを確認しました。
考慮する必要があるもう1つのシナリオは、KittenStore.Intent.Reloadを取得することです。この場合、リストをネットワークから再ロードする必要があります。
テスト手順:
- ソース画像を生成します。
- KittenStoreImplのインスタンスを作成します。
- 新しい画像を生成します。
- Intent.Reloadを送信します。
- 条件に新しい画像が含まれていることを確認してください。
最後に、次のシナリオを確認してみましょう。画像の読み込み中にisLoadingフラグが設定されている場合。
TestSchedulerの手動処理を有効にしました。これで、タスクは自動的に処理されなくなります。これにより、応答を待っている間にステータスを確認できます。
KittenComponent統合テスト
上で述べたように、KittenComponentはモジュール全体の統合ポイントです。統合テストでカバーできます。そのAPIを見てみましょう:
KittenDataSourceとKittenViewの2つの依存関係があります。テストを開始する前に、これらのテスト実装が必要になります。
完全を期すために、この図はモジュール内のデータフローを示しています。
KittenDataSourceの実装をテストします
このコンポーネントは、ネットワーク要求を担当します。プラットフォームごとに個別の実装があり、テスト用に別の実装が必要です。KittenDataSourceインターフェイスは次のようになります。
TheCatAPIはページネーションをサポートしているので、すぐに適切な引数を追加しました。それ以外は、以前に実装したKittenStore.Networkと非常によく似ています。唯一の違いは、統合で実際のコードをテストするため、JSON形式を使用する必要があることです。したがって、実装のアイデアを借りるだけです。
以前と同様に、リクエストごとにJSON配列にエンコードされる文字列のさまざまなリストを生成します。画像が生成されない場合、または要求引数が間違っている場合は、応答なしで終了する可能性があります。kotlinx.serialization
ライブラリは、JSON配列を形成するために使用されます。ちなみに、テスト済みのKittenStoreParserはデコードに使用します。
KittenViewの実装をテストします
これは、テストを開始する前にテストの実装が必要な最後のコンポーネントです。そのインターフェースは次のとおりです。
これは、モデルを取得してイベントを発生させるだけのビューであるため、テストの実装は非常に簡単です。
最後に受け入れられたモデルを覚えておく必要があります。これにより、表示されたモデルを検証できます。継承されたAbstractMviViewクラスで宣言されているdispatch(Event)メソッドを使用して、KittenViewに代わってイベントをディスパッチすることもできます。
KittenComponentの統合テスト
テストの完全なセットはリポジトリにあります。ここでは、最も興味深いものをいくつか紹介します。
前と同じように、依存関係をインスタンス化して初期化することから始めましょう。
現在、モジュールにはmainSchedulerとcompulationSchedulerの2つのスケジューラーが使用されています。それらをオーバーライドする必要があります。
これで、いくつかのテストを作成できます。最初にメインスクリプトをチェックして、起動時にイメージがロードされて表示されることを確認しましょう。
このテストは、KittenStoreのユニットテストを見たときに書いたものと非常によく似ています。モジュール全体が関係するのは今だけです。
テスト手順:
- TestKittenDataSourceで画像へのリンクを生成します。
- KittenComponentを作成して実行します。
- リンクがTestKittenViewに到達していることを確認してください。
もう1つの興味深いシナリオ:KittenViewがRefreshTriggeredイベントを発生させたときに画像を再ロードする必要があります。
ステージ:
- 画像へのソースリンクを生成します。
- KittenComponentを作成して実行します。
- 新しいリンクを生成します。
- KittenViewに代わってEvent.RefreshTriggeredを送信します。
- 新しいリンクがTestKittenViewに到達することを確認してください。
テストの実行
すべてのテストを実行するには、次のGradleタスクを実行する必要があります。
./gradlew :shared:kittens:build
これにより、モジュールがコンパイルされ、サポートされているすべてのプラットフォーム(Androidおよびiosx64)ですべてのテストが実行されます。
そして、これがJaCoCoカバレッジレポートです。
結論
この記事では、Kittensモジュールについてユニットテストと統合テストについて説明しました。提案されたモジュール設計により、次の部分をカバーすることができました。
- KittenStoreImpl-ほとんどのビジネスロジックが含まれています。
- KittenStoreNetwork-高レベルのネットワーク要求を担当します。
- KittenStoreParser-ネットワーク応答の解析を担当します。
- すべての変換と接続。
最後のポイントは非常に重要です。MVI機能のおかげでそれをカバーすることが可能です。ビューの唯一の責任は、データの表示とイベントのディスパッチです。すべてのサブスクリプション、変換、およびリンクはモジュール内で実行されます。したがって、ディスプレイ自体を除くすべてを一般的なテストでカバーできます。
このようなテストには、次の利点があります。
- プラットフォームAPIを使用しないでください。
- 非常に迅速に実行されました。
- 信頼できる(点滅しないでください);
- サポートされているすべてのプラットフォームで実行します。
また、複雑なKotlin /ネイティブメモリモデルとの互換性についてコードをテストすることもできました。ビルド時のセキュリティが不足しているため、これも非常に重要です。コードは、デバッグが難しい例外を除いて、実行時にクラッシュするだけです。
これがあなたのプロジェクトに役立つことを願っています。私の記事を読んでくれてありがとう!そして、Twitterで私をフォローすることを忘れないでください。
..。
ボーナスエクササイズ
テスト実装で作業したり、MVIで遊んだりする場合は、ここにいくつかの実践的な演習があります。
KittenDataSourceのリファクタリング
モジュールには、KittenDataSourceインターフェイスの2つの実装があります。1つはAndroid用、もう1つはiOS用です。彼らがネットワークアクセスに責任があることはすでに述べました。ただし、実際には別の機能があります。入力引数「limit」と「page」に基づいてリクエストのURLを生成します。同時に、KittenDataSourceへの呼び出しを委任する以外に何もしないKittenStoreNetworkクラスがあります。
割り当て:URL要求生成ロジックをKittenDataSourceImpl(AndroidおよびiOSの場合)からKittenStoreNetworkに移動します。KittenDataSourceインターフェイスを次のように変更
する必要があります。変更したら、テストを更新する必要があります。触れる必要がある唯一のクラスはTestKittenDataSourceです。
ページ読み込みの追加
TheCatAPIはページネーションをサポートしているため、この機能を追加してユーザーエクスペリエンスを向上させることができます。KittenViewに新しいEvent.EndReachedイベントを追加することから始めることができます。その後、コードはコンパイルを停止します。次に、適切なIntent.LoadMoreを追加し、新しいイベントをIntentに変換して、KittenStoreImplで後者を処理する必要があります。また、KittenStoreImpl.Networkインターフェイスを次のように変更する必要があります。
最後に、いくつかのテスト実装を更新し、1つまたは2つの既存のテストを修正してから、ページネーションをカバーするためにいくつかの新しいテストを作成する必要があります。
