📝

Android Basic with ComposeのUnit2で学んだことをまとめてみる - 前編(Kotlin/Test)

2024/10/23に公開

はじめに

こんにちは、某SIerでSEをやっているnekorush14です。
この記事は絶賛再*n入門しているAndroid開発について、Google公式のLearning Courseを通して学んだことをアウトプットするシリーズです。

Jetpack Composeに関するCourseのUnit2で得た知見を話します。
https://developer.android.com/courses/android-basics-compose/unit-2?hl=en

前回の記事はこちら👇
https://zenn.dev/nekorush14/articles/6c3f2bc03a1e7d

Unit 2: Building app UI

このユニットでは、シンプルなユーザーインタラクションを含むアプリケーションの実装を通して以下の内容を習得します。

  • Kotlinでの分岐、オブジェクト指向プログラミング、Null許容、関数型の扱い
  • ボタンとボタンをタップしたときのアクション定義の仕方
  • Androidの状態管理の基礎
  • Androidにおけるデバッグとテストの基礎

Kotlinについて少し忘れているところもあったので、今回は短いですがKotlinの章で学んだことも少しだけまとめてみます。

また、Unit2のメインテーマはユーザーインタラクションを含むアプリケーションの仕組みと状態管理(だと思っています)です。今回はUnit2で扱われたModifier、Compose、テスト、そしてステータス管理についてまとめてみます。加えて、Unitの最後の課題で取り組んだアプリケーション作成にて、このUnitのスコープ外のことも少しだけ取り入れてみたのでその話もしてみます。

Kotlin

このユニットでは以下のテーマが扱われました。

  • Generics
  • Data class
  • Singleton Object
  • Companion object
  • 継承なしでクラスを拡張するExtensions
  • Collections

この中で気になったものはData class, Companion object, Extensionsです。
Data classはクラスのプロパティを使ってデータを保持するために使用されるクラスです。このクラスのequals()toString()は自動的に生成されます。特にtoStringは通常のクラスと異なり、オブジェクトのハッシュではなくクラスのプロパティを文字列で返します。
※便利ですね🤔

CCompanion objectはクラスに紐づいたシングルトンオブジェクトを生成するためのものです。 object宣言でもシングルトンオブジェクトを生成できますが、Companion objectを使うことで、クラス名を通してシングルトンオブジェクトにアクセスできます。Companion objectの場合は<class名>.<オブジェクト内のプロパティまたはメソッド名>でアクセスできます。
※Javaで言うStaticな値やメソッドみたいなイメージで使う理解です。
詳しくは、Kotlinの公式ドキュメントData classes | Kotlin Documentationを参照ください。

Extensionsは継承なしでクラスを拡張する構文です。
例えば以下のようなイメージです。

Extensionsの例
class Sample

/**
 * [Sample]クラス宣言時に定義されていない`getSample`メソッドを定義する。
 */
fun Sample.getSample(): String {
  // ...
}

fun main() {
    
    val sample = Sample()

    // 呼び出しはこの例だとSampleオブジェクトを作成してから通常のメソッド呼び出しと同じ記法で
    // メソッドを呼び出せる
    println(sample.getSample())
}

Codelabsによると、Jetpack ComposeではModifierがExtensionsを使用して実装されているそうです。

Test

Androidで提供されている標準のテストは以下の2つです。

  • Local test
  • Instrumentation test

Local testはいわゆるUnit Testと同義で、作成した関数内のロジック・クラス・プロパティなどをテストします。
JUnitを使用しているため、Android Studioで完結し、かつテスト実行にエミュレータやAndroid実機は不要です。

Instrumentation testは直訳すると機器テストです。Androidにおいては作成したUIのテスト、Android固有のAPIやプラットフォームのAPI/サービスに依存するテストを意味します。
UIやAndroidがサポートするAPIに絡んだテストを行うため、テストの実行にエミュレータやAndroid実機が必要です。

Instrumentation testを実行すると、Android Studioにおけるエミュレータでのアプリ実行時と同様にAPK[1]にパッケージングされます。
通常のアプリケーションAPKに加えてテスト用のAPKも生成されます。テストを実行するデバイスには通常のアプリケーションAPKがインストールされ、テスト用のAPKが通常のアプリケーションAPKに対してテストを実行します。

テストを記載したコード(テストコード)からテスト対象のコード(プロダクトコード)へアクセスする際、プロダクトコードがprivateで宣言されているとアクセスできません。
privateなメソッドはテストすべきなのかという議論がありますが、ここではスコープ外とします。

そこで、このCodelabsではprivateで宣言されている箇所をinternalへ変更し、@VisibleForTestingアノテーションを付与することでこの問題を回避しています。
@VisibleForTestingで宣言されたメソッドを、テスト目的でのみ外部からアクセスできるようにすることを明示しています。

Local test

Local testのテストディレクトリはsrc/test/java/<PackageName>/に作成します。
テストコードの階層はプロダクトコードコードと一致化させる必要があるため、以下のようにmain配下と同様のディレクトリ構成とします。


Project view表示

テストコードはまずテストクラスを作成します。テストクラス内でテストケースごとにテストメソッドを作成します。
テストメソッドには@Testアノテーションを付与します。実行結果が予期した値となっているか確認するアサーションですが、Androidのテストではassertメソッドを使用します。
基本的にはJUnitを使用しているため以下のようなアサーションが使用できます。

  • assertEquals() - 引数の値どおししが一致しているか
  • assertNotEquals() - 引数の値どおしが一致していないか
  • assertTrue() - 引数の値がTrueであるか
  • assertFalse() - 引数の値がFalseであるか
  • assertNull() - 引数の値がNullか
  • assertNotNull() - 引数の値がNull以外か
  • assertThat() - Hamcrestライブラリ等を使用したMatcherによるアサーション

テストの実行はIDEのガターにあるアイコンから実行する。
この際、Debug実行も可能です。つまりプロダクトコードにBreakPointを付与して値を確認できます。

Local testのテストコード例
class SampleTests {
    @Test
    fun sampleCalculation_sumReturnString() {
        val value1 = 10.00
        val value2 = 20.00

        val expectedString = "30.00"
        val actualString = sampleCalculation(x = value1, y = value2)
        assertEquals(expectedString, actualString)
    }
}

Instrumentation test

Instrumentation testのテストディレクトリはsrc/androidTest/java/<PackageName>に作成します。
テストコードの階層はプロダクトコードコードと一致化させる必要があるため、以下のようにmain配下と同様のディレクトリ構成とします。


Project view表示

Instrumentation testのテストクラスもLocal Testと同様に作成します。
注意点は、UIテストのため、onCreate()が呼ばれてUIが設定されたあとにテストを実行する必要があります。
テストコード内でcreateComposeRule()を変数に格納し、createComposeRule().setContentによりプロダクトコードのUIを設定します。

UIのコンポーネントにテストコードからアクセスするにはノードを通して行います。
onNodeWithText()により引数のStringを持つコンポーネントにアクセスできます。
また、performTextInput()により引数に指定した値をそのコンポーネントに渡すことができます。

ノードを通してアクセスしたコンポーネントから直接assertするメソッドを使用できます。
このassertメソッドの引数はアサーションがFalseとなる場合のメッセージを入れます。

Instrumentation testのテストコード例
class SampleUITests {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun calculate_sample_30_value() {
        composeTestRule.setContent {
            SampleTheme {
                Surface (modifier = Modifier.fillMaxSize()){
                    SampleLayout()
                }
            }
        }
        composeTestRule.onNodeWithText("Sample X").performTextInput("10")
        composeTestRule.onNodeWithText("Sample Y").performTextInput("20")

        val expectedValue = "30"
        composeTestRule.onNodeWithText("Sample Value: $expectedValue").assertExists(
            "No node with this text was found."
        )
    }
}

まとめ

今回はUnit2で扱われたKotlinとAndroidでのTestについてまとめてみました。

Androidでリストなどを使用する際に必要となる知識やデータを扱う際に使用するであろうクラスについてKotlinの記法に触れられていました。
特にDataクラスやCollectionsの話はデータを扱う際に基本となると考えています。
TestではAndroidにおける単体テストとUIテストの書き方について基礎的なことを学べました。

次回も引き続きUnit2で、メイントピックとなるComposeの状態管理についてまとめます。

参考資料

脚注
  1. APK(Android Application Package)はその名の通り、Androidのアプリケーションをパッケージ化したファイルです。 ↩︎

Discussion