KotlinマルチプラットフォームのMVIアーキテクチャパターン。パート3:テスト





この記事は、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のユニットテストの作成



  • KittenComponentのテスト
    • KittenDataSourceのテスト実装の作成

    • テストKittenView実装を構築する

    • KittenComponentの統合テストの作成



  • テストの実行

  • 結論





KittenStoreユニットテスト



KittenStoreインターフェースには、独自の実装クラスであるKittenStoreImplがあります。これが私たちがテストしようとしているものです。クラス自体で直接定義された2つの依存関係(内部インターフェイス)があります。それらのテスト実装を作成することから始めましょう。



KittenStore.Parserの実装をテストします



このコンポーネントは、ネットワーク要求を担当します。これはそのインターフェースがどのように見えるかです:



interface Network {
fun load(): Maybe<String>
}
インターフェイス ネットワーク{
fun load() たぶん< String >
}
生で見る GitHub によって❤でホストされているKittenStoreImpl.kt


ネットワークインターフェイスのテスト実装を作成する前に、1つの重要な質問に答える必要があります。サーバーはどのデータを返すのでしょうか。答えは、サーバーが画像リンクのランダムなセットを、毎回異なるセットを返すということです。実際にはJSON形式が使用されますが、パーサー抽象化があるため、ユニットテストでは形式を気にしません。



実際の実装ではストリームを切り替えることができるため、サブスクライバーはKotlin / Nativeでフリーズできますこの動作をモデル化して、コードがすべてを正しく処理することを確認するのは素晴らしいことです。



したがって、ネットワークのテスト実装には次の機能が必要です。



  • リクエストごとに、空でない異なる行のセットを返す必要があります。

  • 応答形式は、ネットワークとパーサーで共通である必要があります。

  • ネットワークエラーをシミュレートできる必要があります(おそらく応答なしで完了する必要があります)。

  • 無効な応答形式をシミュレートできる必要があります(パーサーのエラーをチェックするため)。

  • (ブートフェーズをテストするために)応答遅延をシミュレートできる必要があります。

  • Kotlin / Nativeでフリーズ可能である必要があります(念のため)。



テストの実装自体は次のようになります。



class TestKittenStoreNetwork(
private val scheduler: TestScheduler
) : KittenStoreImpl.Network {
var images: List<String>? by AtomicReference<List<String>?>(null)
private var seed: Int by AtomicInt()
override fun load(): Maybe<String> =
singleFromFunction { images }
.notNull()
.map { it.joinToString(separator = SEPARATOR) }
.observeOn(scheduler)
fun generateImages(): List<String> {
val images = List(MAX_IMAGES) { "Img${seed + it}" }
this.images = images
seed += MAX_IMAGES
return images
}
private companion object {
private const val MAX_IMAGES = 50
private const val SEPARATOR = ";"
}
}
クラス TestKittenStoreNetwork
プライベート ヴァル・ スケジューラ TestScheduler
KittenStoreImplネットワーク{
var images: List<String>? by AtomicReference<List<String>?>(null)
private var seed: Int by AtomicInt()
override fun load(): Maybe<String> =
singleFromFunction { images }
.notNull()
.map { it.joinToString(separator = SEPARATOR) }
.observeOn(scheduler)
fun generateImages(): List<String> {
val images = List(MAX_IMAGES) { "Img${seed + it}" }
this.images = images
seed += MAX_IMAGES
return images
}
private companion object {
private const val MAX_IMAGES = 50
private const val SEPARATOR = ";"
}
}
view raw GitHub によって❤でホストされているTestKittenStoreNetwork.kt


TestKittenStoreNetworkには(実サーバーと同じように)文字列ストレージがあり、文字列を生成できます。リクエストごとに、現在の行リストが1行にエンコードされます。「images」プロパティがゼロの場合、たぶん終了するだけで、エラーと見なされます。TestScheduler



も使用しましたこのスケジューラには1つの重要な機能があります。それは、すべての着信タスクをフリーズすることです。したがって、TestSchedulerと組み合わせて使用​​されるobserveOnオペレーターは、実際の生活と同じように、ダウンストリームと、それを通過するすべてのデータをフリーズします。しかし同時に、マルチスレッドは関与しないため、テストが簡素化され、信頼性が向上します。



さらに、TestSchedulerには、ネットワークの待ち時間をシミュレートできる特別な「手動処理」モードがあります。



KittenStore.Parserの実装をテストします



このコンポーネントは、サーバーからの応答の解析を担当します。そのインターフェースは次のとおりです。



interface Parser {
fun parse(json: String): Maybe<List<String>>
}
インターフェイス パーサー{
fun parsejson String たぶん<リスト<文字列>>
}
生で見る GitHub によって❤でホストされているKittenStoreImpl.kt


したがって、Webからダウンロードしたものはすべて、リンクのリストに変換する必要があります。私たちのネットワークは、セミコロン(;)区切り文字を使用して文字列を連結するだけなので、ここでは同じ形式を使用します。



テストの実装は次のとおりです。



class TestKittenStoreParser : KittenStoreImpl.Parser {
override fun parse(json: String): Maybe<List<String>> =
json
.toSingle()
.filter { it != "" }
.map { it.split(SEPARATOR) }
.observeOn(TestScheduler())
private companion object {
private const val SEPARATOR = ";"
}
}
class TestKittenStoreParser : KittenStoreImpl.Parser {
override fun parse(json: String): Maybe<List<String>> =
json
.toSingle()
.filter { it != "" }
.map { it.split(SEPARATOR) }
.observeOn(TestScheduler())
private companion object {
private const val SEPARATOR = ";"
}
}
view raw GitHub によって❤でホストされているTestKittenStoreParser.kt


ネットワークと同様に、TestSchedulerを使用してサブスクライバーをフリーズし、Kotlin /ネイティブメモリモデルとの互換性を確認します。入力文字列が空の場合、応答処理エラーがシミュレートされます。



KittenStoreImplのユニットテスト



これで、すべての依存関係のテスト実装ができました。ユニットテストの時間です。すべてのユニットテストはリポジトリにあります。ここでは、初期化といくつかのテストのみを示します。



最初のステップは、テスト実装のインスタンスを作成することです。



class KittenStoreTest {
private val parser = TestKittenStoreParser()
private val networkScheduler = TestScheduler()
private val network = TestKittenStoreNetwork(networkScheduler)
private fun store(): KittenStore = KittenStoreImpl(network, parser)
// ...
}
クラス KittenStoreTest {
private val parser = TestKittenStoreParser()
private val networkScheduler = TestScheduler()
private val network = TestKittenStoreNetwork(networkScheduler)
プライベート ファン ストア() KittenStore = KittenStoreImpl(ネットワーク、パーサー)
// ..。
}
生で見る GitHub によって❤でホストされているKittenStoreTest.kt


KittenStoreImplはmainSchedulerを使用するため、次のステップはそれをオーバーライドすることです。



class KittenStoreTest {
private val network = TestKittenStoreNetwork()
private val parser = TestKittenStoreParser()
private fun store(): KittenStore = KittenStoreImpl(network, parser)
@BeforeTest
fun before() {
overrideSchedulers(main = { TestScheduler() })
}
@AfterTest
fun after() {
overrideSchedulers()
}
// ...
}
クラス KittenStoreTest {
private val network = TestKittenStoreNetwork()
private val parser = TestKittenStoreParser()
private fun store(): KittenStore = KittenStoreImpl(network, parser)
@BeforeTest
fun before() {
overrideSchedulers(main = { TestScheduler() })
}
@AfterTest
fun after() {
overrideSchedulers()
}
// ...
}
view raw KittenStoreTest.kt hosted with ❤ by GitHub


これで、いくつかのテストを実行できます KittenStoreImplは、作成後すぐにイメージをロードする必要があります。つまり、ネットワークリクエストを実行し、その応答を処理して、状態を新しい結果で更新する必要があります。



@Test
fun loads_images_WHEN_created() {
val images = network.generateImages()
val store = store()
assertEquals(State.Data.Images(urls = images), store.state.data)
}
@テスト
楽しい loads_images_WHEN_created(){
val images = network.generateImages()
val store = store()
assertEquals(State.Data.Images(のURL =イメージ)、STORE.STATE。データ
}
生で見る GitHub によって❤でホストされているKittenStoreTest.kt


我々のしたこと:



  • ネットワーク上で生成された画像。

  • KittenStoreImplの新しいインスタンスを作成しました。

  • 状態に文字列の正しいリストが含まれていることを確認しました。



考慮する必要があるもう1つのシナリオは、KittenStore.Intent.Reloadを取得することです。この場合、リストをネットワークから再ロードする必要があります。



@Test
fun reloads_images_WHEN_Intent_Reload() {
network.generateImages()
val store = store()
val newImages = network.generateImages()
store.onNext(Intent.Reload)
assertEquals(State.Data.Images(urls = newImages), store.state.data)
}
@テスト
楽しい reloads_images_WHEN_Intent_Reload(){
network.generateImages()
val store = store()
val newImages = network.generateImages()
store.onNext(Intent.Reload
assertEquals(State.Data.Images(のURL = newImages)、STORE.STATE。データ
}
生で見る GitHub によって❤でホストされているKittenStoreTest.kt


テスト手順:



  • ソース画像を生成します。

  • KittenStoreImplのインスタンスを作成します。

  • 新しい画像を生成します。

  • Intent.Reloadを送信します。

  • 条件に新しい画像が含まれていることを確認してください。



最後に、次のシナリオを確認してみましょう。画像の読み込み中にisLoadingフラグが設定されている場合。



@Test
fun isLoading_true_WHEN_loading() {
networkScheduler.isManualProcessing = true
network.generateImages()
val store = store()
assertTrue(store.state.isLoading)
}
@テスト
fun isLoading_true_WHEN_loading(){
networkScheduler.isManualProcessing = true
network.generateImages()
val store = store()
assertTrue(store.state.isLoading)
}
生で見る GitHub によって❤でホストされているKittenStoreTest.kt


TestSchedulerの手動処理を有効にしました。これで、タスクは自動的に処理されなくなります。これにより、応答を待っている間にステータスを確認できます。



KittenComponent統合テスト



上で述べたように、KittenComponentはモジュール全体の統合ポイントです。統合テストでカバーできます。そのAPIを見てみましょう:



internal class KittenComponent internal constructor(dataSource: KittenDataSource) {
constructor() : this(KittenDataSource())
fun onViewCreated(view: KittenView) { /* ... */ }
fun onStart() { /* ... */ }
fun onStop() { /* ... */ }
fun onViewDestroyed() { /* ... */ }
fun onDestroy() { /* ... */ }
}
内部 クラス KittenComponent 内部 コンストラクターdataSource KittenDataSource){
コンストラクター() thisKittenDataSource())
fun onViewCreatedview KittenView){ / * ... * / }
fun onStart(){ / * ... * / }
fun onStop(){ / * ... * / }
fun onViewDestroyed(){ / * ... * / }
fun onDestroy(){ / * ... * / }
}
生で見る GitHub によって❤でホストされているKittenComponent.kt


KittenDataSourceとKittenViewの2つの依存関係があります。テストを開始する前に、これらのテスト実装が必要になります。



完全を期すために、この図はモジュール内のデータフローを示しています。







KittenDataSourceの実装をテストします



このコンポーネントは、ネットワーク要求を担当します。プラットフォームごとに個別の実装があり、テスト用に別の実装が必要です。KittenDataSourceインターフェイスは次のようになります。



internal interface KittenDataSource {
fun load(limit: Int, offset: Int): Maybe<String>
}
内部 インターフェイス KittenDataSource {
楽しい 負荷制限 Intオフセット Int たぶん<文字列>
}


TheCatAPIはページネーションをサポートしているので、すぐに適切な引数を追加しました。それ以外は、以前に実装したKittenStore.Networkと非常によく似ています。唯一の違いは、統合で実際のコードをテストするため、JSON形式を使用する必要があることです。したがって、実装のアイデアを借りるだけです。



internal class TestKittenDataSource(
private val scheduler: TestScheduler
) : KittenDataSource {
private var images by AtomicReference<List<String>?>(null)
private var seed by AtomicInt()
override fun load(limit: Int, page: Int): Maybe<String> =
singleFromFunction { images }
.notNull()
.map {
val offset = page * limit
it.subList(fromIndex = offset, toIndex = offset + limit)
}
.mapIterable { it.toJsonObject() }
.map { JsonArray(it).toString() }
.onErrorComplete()
.observeOn(scheduler)
private fun String.toJsonObject(): JsonObject =
JsonObject(mapOf("url" to JsonPrimitive(this)))
fun generateImages(): List<String> {
val images = List(MAX_IMAGES) { "Img${seed + it}" }
this.images = images
seed += MAX_IMAGES
return images
}
private companion object {
private const val MAX_IMAGES = 50
}
}
内部 クラス TestKittenDataSource
プライベート ヴァル・ スケジューラ TestScheduler
) : KittenDataSource {
private var images by AtomicReference<List<String>?>(null)
private var seed by AtomicInt()
override fun load(limit: Int, page: Int): Maybe<String> =
singleFromFunction { images }
.notNull()
.map {
val offset = page * limit
it.subList(fromIndex = offset, toIndex = offset + limit)
}
.mapIterable { it.toJsonObject() }
.map { JsonArray(it).toString() }
.onErrorComplete()
.observeOn(scheduler)
private fun String.toJsonObject(): JsonObject =
JsonObject(mapOf("url" to JsonPrimitive(this)))
fun generateImages(): List<String> {
val images = List(MAX_IMAGES) { "Img${seed + it}" }
this.images = images
seed += MAX_IMAGES
return images
}
private companion object {
private const val MAX_IMAGES = 50
}
}


以前と同様に、リクエストごとにJSON配列にエンコードされる文字列のさまざまなリストを生成します。画像が生成されない場合、または要求引数が間違っている場合は、応答なしで終了する可能性があります。kotlinx.serialization



ライブラリは、JSON配列を形成するために使用されますちなみに、テスト済みのKittenStoreParserはデコードに使用します。



KittenViewの実装をテストします



これは、テストを開始する前にテストの実装が必要な最後のコンポーネントです。そのインターフェースは次のとおりです。



interface KittenView : MviView<Model, Event> {
data class Model(
val isLoading: Boolean,
val isError: Boolean,
val imageUrls: List<String>
)
sealed class Event {
object RefreshTriggered : Event()
}
}
インターフェイス KittenView MviView <モデルイベント> {
データ クラス モデル
val isLoading Boolean
val isError ブール値
val imageUrls リスト<文字列>
封印された クラス イベント{
オブジェクトRefreshTriggered イベント()
}
}
生で見る GitHub によって❤でホストされているMviKmpKittenViewInterface.kt


これは、モデルを取得してイベントを発生させるだけのビューであるため、テストの実装は非常に簡単です。



class TestKittenView : AbstractMviView<Model, Event>(), KittenView {
lateinit var model: Model
override fun render(model: Model) {
this.model = model
}
}
クラス TestKittenView AbstractMviView <モデルイベント>()、KittenView {
lateinit var model モデル
楽しい レンダリングをオーバーライドするモデル モデル){
this .model = model
}
}
生で見る GitHub によって❤でホストされているTestKittenView.kt


最後に受け入れられたモデルを覚えておく必要があります。これにより、表示されたモデルを検証できます。継承されたAbstractMviViewクラスで宣言されているdispatch(Event)メソッドを使用して、KittenViewに代わってイベントをディスパッチすることもできます。



KittenComponentの統合テスト



テストの完全なセットはリポジトリにあります。ここでは、最も興味深いものをいくつか紹介します。



前と同じように、依存関係をインスタンス化して初期化することから始めましょう。



class KittenComponentTest {
private val dataSourceScheduler = TestScheduler()
private val dataSource = TestKittenDataSource(dataSourceScheduler)
private val view = TestKittenView()
private fun startComponent(): KittenComponent =
KittenComponent(dataSource).apply {
onViewCreated(view)
onStart()
}
// ...
}
クラス KittenComponentTest {
private val dataSourceScheduler = TestScheduler()
private val dataSource = TestKittenDataSource(dataSourceScheduler)
private val view = TestKittenView()
private fun startComponent() KittenComponent =
KittenComponent(dataSource)。適用{
onViewCreated(ビュー)
onStart()
}
// ..。
}
生で見る GitHub によって❤でホストされているKittenComponentTest.kt


現在、モジュールにはmainSchedulerとcompulationSchedulerの2つのスケジューラーが使用されています。それらをオーバーライドする必要があります。



class KittenComponentTest {
private val dataSourceScheduler = TestScheduler()
private val dataSource = TestKittenDataSource(dataSourceScheduler)
private val view = TestKittenView()
private fun startComponent(): KittenComponent =
KittenComponent(dataSource).apply {
onViewCreated(view)
onStart()
}
// ...
@BeforeTest
fun before() {
overrideSchedulers(main = { TestScheduler() }, computation = { TestScheduler() })
}
@AfterTest
fun after() {
overrideSchedulers()
}
}
クラス KittenComponentTest {
private val dataSourceScheduler = TestScheduler()
private val dataSource = TestKittenDataSource(dataSourceScheduler)
private val view = TestKittenView()
private fun startComponent() KittenComponent =
KittenComponent(dataSource)。適用{
onViewCreated(ビュー)
onStart()
}
// ..。
@BeforeTest
前に楽しい(){
overrideSchedulers(main = { TestScheduler()}、computation = { TestScheduler()})
}
@AfterTest
後の楽しみ(){
overrideSchedulers()
}
}
生で見る GitHub によって❤でホストされているKittenComponentTest.kt


これで、いくつかのテストを作成できます。最初にメインスクリプトをチェックして、起動時にイメージがロードされて表示されることを確認しましょう。



@Test
fun loads_and_shows_images_WHEN_created() {
val images = dataSource.generateImages()
startComponent()
assertEquals(images, view.model.imageUrls)
}
@テスト
楽しい loads_and_shows_images_WHEN_created(){
val images = dataSource.generateImages()
startComponent()
assertEquals(images、view.model.imageUrls)
}
生で見る GitHub によって❤でホストされているKittenComponentTest.kt


このテストは、KittenStoreのユニットテストを見たときに書いたものと非常によく似ています。モジュール全体が関係するのは今だけです。



テスト手順:



  • TestKittenDataSourceで画像へのリンクを生成します。

  • KittenComponentを作成して実行します。

  • リンクがTestKittenViewに到達していることを確認してください。



もう1つの興味深いシナリオ:KittenViewがRefreshTriggeredイベントを発生させたときに画像を再ロードする必要があります。



@Test
fun reloads_images_WHEN_Event_RefreshTriggered() {
dataSource.generateImages()
startComponent()
val newImages = dataSource.generateImages()
view.dispatch(Event.RefreshTriggered)
assertEquals(newImages, view.model.imageUrls)
}
@テスト
楽しい reloads_images_WHEN_Event_RefreshTriggered(){
dataSource.generateImages()
startComponent()
val newImages = dataSource.generateImages()
view.dispatch(Event.RefreshTriggered
assertEquals(newImages、view.model.imageUrls)
}
生で見る GitHub によって❤でホストされているKittenComponentTest4.kt


ステージ:



  • 画像へのソースリンクを生成します。

  • 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つの既存のテストを修正してから、ページネーションをカバーするためにいくつかの新しいテストを作成する必要があります。






All Articles