この記事は、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の実装をテストします
このコンポーネントは、ネットワーク要求を担当します。 これはそのインターフェースがどのように見えるかです:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface Network {
fun load(): Maybe<String>
}
インターフェイス ネットワーク {
fun load () : たぶん < String >
}
ネットワークインターフェイスのテスト実装を作成する前に、1つの重要な質問に答える必要があります。サーバーはどのデータを返すのでしょうか。 答えは、サーバーが画像リンクのランダムなセットを、毎回異なるセットを返すということです。 実際にはJSON形式が使用されますが、パーサー抽象化があるため、ユニットテストでは形式を気にしません。
実際の実装ではストリームを切り替えることができるため、サブスクライバーは Kotlin / Nativeで フリーズ できます 。 この動作をモデル化して、コードがすべてを正しく処理することを確認するのは素晴らしいことです。
したがって、ネットワークのテスト実装には次の機能が必要です。
リクエストごとに、空でない異なる行のセットを返す必要があります。
応答形式は、ネットワークとパーサーで共通である必要があります。
ネットワークエラーをシミュレートできる必要があります(おそらく応答なしで完了する必要があります)。
無効な応答形式をシミュレートできる必要があります(パーサーのエラーをチェックするため)。
(ブートフェーズをテストするために)応答遅延をシミュレートできる必要があります。
Kotlin / Nativeでフリーズ可能である必要があります(念のため)。
テストの実装自体は次のようになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = " ;"
}
}
TestKittenStoreNetworkには(実サーバーと同じように)文字列ストレージがあり、文字列を生成できます。 リクエストごとに、現在の行リストが1行にエンコードされます。 「images」プロパティがゼロの場合、たぶん終了するだけで、エラーと見なされます。 TestScheduler
も使用し ました 。 このスケジューラには1つの重要な機能があります。それは、すべての着信タスクをフリーズすることです。 したがって、TestSchedulerと組み合わせて使用されるobserveOnオペレーターは、実際の生活と同じように、ダウンストリームと、それを通過するすべてのデータをフリーズします。 しかし同時に、マルチスレッドは関与しないため、テストが簡素化され、信頼性が向上します。
さらに、TestSchedulerには、ネットワークの待ち時間をシミュレートできる特別な「手動処理」モードがあります。
KittenStore.Parserの実装をテストします
このコンポーネントは、サーバーからの応答の解析を担当します。 そのインターフェースは次のとおりです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface Parser {
fun parse(json: String): Maybe<List<String>>
}
インターフェイス パーサー {
fun parse ( json : String ) : たぶん < リスト < 文字列 >>
}
したがって、Webからダウンロードしたものはすべて、リンクのリストに変換する必要があります。 私たちのネットワークは、セミコロン(;)区切り文字を使用して文字列を連結するだけなので、ここでは同じ形式を使用します。
テストの実装は次のとおりです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = " ;"
}
}
ネットワークと同様に、TestSchedulerを使用してサブスクライバーをフリーズし、Kotlin /ネイティブメモリモデルとの互換性を確認します。 入力文字列が空の場合、応答処理エラーがシミュレートされます。
KittenStoreImplのユニットテスト
これで、すべての依存関係のテスト実装ができました。 ユニットテストの時間です。 すべてのユニットテスト はリポジトリにあります 。ここでは、初期化といくつかのテストのみを示します。
最初のステップは、テスト実装のインスタンスを作成することです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 (ネットワーク、パーサー)
// ..。
}
KittenStoreImplはmainSchedulerを使用するため、次のステップはそれをオーバーライドすることです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()
}
// ...
}
これで、いくつかのテストを実行できます KittenStoreImplは、作成後すぐにイメージをロードする必要があります。 つまり、ネットワークリクエストを実行し、その応答を処理して、状態を新しい結果で更新する必要があります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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。 データ )
}
我々のしたこと:
ネットワーク上で生成された画像。
KittenStoreImplの新しいインスタンスを作成しました。
状態に文字列の正しいリストが含まれていることを確認しました。
考慮する必要があるもう1つのシナリオは、KittenStore.Intent.Reloadを取得することです。 この場合、リストをネットワークから再ロードする必要があります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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。 データ )
}
テスト手順:
ソース画像を生成します。
KittenStoreImplのインスタンスを作成します。
新しい画像を生成します。
Intent.Reloadを送信します。
条件に新しい画像が含まれていることを確認してください。
最後に、次のシナリオを確認してみましょう。画像の読み込み中にisLoadingフラグが設定されている場合。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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)
}
TestSchedulerの手動処理を有効にしました。これで、タスクは自動的に処理されなくなります。 これにより、応答を待っている間にステータスを確認できます。
KittenComponent統合テスト
上で述べたように、KittenComponentはモジュール全体の統合ポイントです。 統合テストでカバーできます。 そのAPIを見てみましょう:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ){
コンストラクター () : this ( KittenDataSource ())
fun onViewCreated ( view : KittenView ){ / * ... * / }
fun onStart (){ / * ... * / }
fun onStop (){ / * ... * / }
fun onViewDestroyed (){ / * ... * / }
fun onDestroy (){ / * ... * / }
}
KittenDataSourceとKittenViewの2つの依存関係があります。 テストを開始する前に、これらのテスト実装が必要になります。
完全を期すために、この図はモジュール内のデータフローを示しています。
KittenDataSourceの実装をテストします
このコンポーネントは、ネットワーク要求を担当します。 プラットフォームごとに個別の実装があり、テスト用に別の実装が必要です。 KittenDataSourceインターフェイスは次のようになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal interface KittenDataSource {
fun load(limit: Int, offset: Int): Maybe<String>
}
内部 インターフェイス KittenDataSource {
楽しい 負荷 ( 制限 : Int 、 オフセット : Int ) : たぶん < 文字列 >
}
TheCatAPI はページネーションをサポートしているので、すぐに適切な引数を追加しました。 それ以外は、以前に実装したKittenStore.Networkと非常によく似ています。 唯一の違いは、統合で実際のコードをテストするため、JSON形式を使用する必要があることです。 したがって、実装のアイデアを借りるだけです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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の実装をテストします
これは、テストを開始する前にテストの実装が必要な最後のコンポーネントです。 そのインターフェースは次のとおりです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 : イベント ()
}
}
これは、モデルを取得してイベントを発生させるだけのビューであるため、テストの実装は非常に簡単です。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
}
}
最後に受け入れられたモデルを覚えておく必要があります。これにより、表示されたモデルを検証できます。 継承されたAbstractMviViewクラスで宣言されているdispatch(Event)メソッドを使用して、KittenViewに代わってイベントをディスパッチすることもできます。
KittenComponentの統合テスト
テストの完全なセットは リポジトリにあります 。ここでは、最も興味深いものをいくつか紹介します。
前と同じように、依存関係をインスタンス化して初期化することから始めましょう。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()
}
// ..。
}
現在、モジュールにはmainSchedulerとcompulationSchedulerの2つのスケジューラーが使用されています。 それらをオーバーライドする必要があります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()
}
}
これで、いくつかのテストを作成できます。 最初にメインスクリプトをチェックして、起動時にイメージがロードされて表示されることを確認しましょう。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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)
}
このテストは、KittenStoreのユニットテストを見たときに書いたものと非常によく似ています。 モジュール全体が関係するのは今だけです。
テスト手順:
TestKittenDataSourceで画像へのリンクを生成します。
KittenComponentを作成して実行します。
リンクがTestKittenViewに到達していることを確認してください。
もう1つの興味深いシナリオ:KittenViewがRefreshTriggeredイベントを発生させたときに画像を再ロードする必要があります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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)
}
ステージ:
画像へのソースリンクを生成します。
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つの既存のテストを修正してから、ページネーションをカバーするためにいくつかの新しいテストを作成する必要があります。