自動テストを作成する

1. 始める前に

この Codelab では、Android の自動テストの概要と、自動テストを使用してスケーラブルで堅牢なアプリを作成する方法を学習します。また、UI ロジックとビジネス ロジックの違いを理解し、両方のロジックをテストする方法を習得します。最後に、Android Studio で自動テストを作成して実行する方法を学習します。

前提条件

  • 関数とコンポーザブルを使用して Android アプリを作成する能力。

学習内容

  • Android の自動テストの概要
  • 自動テストが重要である理由
  • ローカルテストの概要と用途
  • インストルメンテーション テストの概要と用途
  • Android コード用のローカルテストを作成する方法
  • Android アプリ用のインストルメンテーション テストを作成する方法
  • 自動テストを実行する方法

作成するアプリの概要

  • ローカルテスト
  • インストルメンテーション テスト

必要なもの

  • Android Studio の最新バージョン
  • Tip Time アプリの解答コード

2. スターター コードを取得する

コードをダウンロードします。

または、GitHub リポジトリのクローンを作成してコードを入手することもできます。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout main

3. 自動テスト

ソフトウェアのテストとは、ソフトウェアが想定どおりに動作することをチェックする体系的な方法です。自動テストとは、作成したコードの別の部分が正しく動作することを確認するためのコードです。

テストは、アプリの開発における重要なプロセスです。アプリに対して一貫性のあるテストを実施することで、アプリの公開前に、その正確性、機能の動作、使いやすさを検証できます。

テストでは、変更が導入されるたびに既存のコードを継続的にチェックすることもできます。

手動テストはほぼ常に実施されていますが、Android でのテストは多くの場合、自動化が可能です。コースの残り全体を通して、アプリコードとアプリ自体の機能要件をテストする自動テストに焦点を当てます。この Codelab では、Android でのテストの基本について説明します。後の Codelab では、Android アプリをテストするためのより高度な方法を学びます。

Android 開発と Android アプリのテストに慣れてきたら、日頃からアプリのコードとともにテストを作成することをおすすめします。アプリに新しい機能を追加するたびにテストを作成することで、以降のアプリの拡張に伴うワークロードを削減できます。また、手動によるアプリのテストにあまり時間をかけずに、アプリが適切に動作することを簡単に確認できます。

自動テストはすべてのソフトウェア開発に不可欠な要素であり、Android 開発も例外ではありません。したがって、今すぐ自動テストを導入することをおすすめします。

自動テストが重要である理由

最初は、自分のアプリにテストはあまり必要ないように思われるかもしれませんが、アプリのサイズと複雑さにかかわらず、テストは必要です。

コードベースを拡張するには、新しい要素を追加するたびに、既存の機能をテストする必要があります。ただし、これは、既存のテストがある場合にのみ可能です。アプリのサイズが大きくなると、手動テストでは自動テストよりはるかに多くの労力が必要になります。さらに、本番環境でのアプリの開発が始まったら、大規模なユーザーベースが存在する場合は、テストが必要不可欠になります。たとえば、Android の多くの異なるバージョンを搭載したさまざまな種類のデバイスについて考慮しなければなりません。

規模が大きくなると、最終的には、手動テストより自動テストの方が大幅に速く使用シナリオの大部分をカバーできるようになります。新しいコードをリリースする前にテストを実施すると、既存のコードに変更を加えて、予期しない動作をするアプリがリリースされることを防止できます。

自動テストは、人がデバイスを直接操作して実施する手動テストとは対照的に、ソフトウェアを通じて実行されるテストであることに留意してください。自動テストと手動テストは、ユーザーがプロダクトを快適に利用できるようにするために重要な役割を果たします。ただし、自動テストの方が正確であり、チームの生産性を最適化できます。これは、人が実行する必要がなく、手動テストよりもはるかに速く実行できるためです。

自動テストの種類

ローカルテスト

ローカルテストは自動テストの一種であり、コードの小規模な一部を直接テストして、その部分が適切に機能するかどうかを確認するものです。ローカルテストでは、関数、クラス、プロパティをテストできます。ローカルテストはワークステーションで実行されます。つまり、デバイスやエミュレータを必要とすることなく、開発環境で実行されます。簡単に言うと、ローカルテストは個人のパソコンで実行できます。また、コンピュータ リソースのオーバーヘッドが非常に少ないため、限られたリソースでも高速で実行できます。Android Studio は、ローカルテストを自動的に実行する機能を備えています。

インストルメンテーション テスト

Android 開発の場合、インストルメンテーション テストは UI テストです。インストルメンテーション テストでは、Android API と、そのプラットフォームの API およびサービスに依存するアプリの要素をテストできます。

ローカルテストと異なり、UI テストではアプリの全体または一部を起動し、ユーザー操作をシミュレートして、アプリが適切に反応するかどうかをチェックします。このコース全体を通して、UI テストは物理デバイスまたはエミュレータで実行します。

Android でインストルメンテーション テストを実行する場合、通常の Android アプリと同様に、テストコードは固有の Android Application Package(APK)に実際に組み込まれます。APK は、すべてのコードと、デバイスまたはエミュレータでアプリを実行するために必要なすべてのファイルを含む圧縮ファイルです。テスト APK は、通常のアプリ APK とともに、デバイスまたはエミュレータにインストールされます。これにより、テスト APK はアプリ APK に対してテストを実行します。

4. ローカルテストを作成する

アプリコードを準備する

ローカルテストでは、アプリコードからメソッドを直接テストするので、テストクラスとテストメソッドからテスト対象のメソッドにアクセスできることが必要です。次のコード スニペットのローカルテストでは、calculateTip() メソッドが正しく動作するかどうかを確認しますが、calculateTip() メソッドは現在 private であるため、テストからアクセスできません。private の指定を削除し、internal にします。

MainActivity.kt

internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}
  • MainActivity.kt ファイルの calculateTip() メソッドの前の行に、@VisibleForTesting アノテーションを追加します。
@VisibleForTesting
internal fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}

これによりメソッドはパブリックになりますが、テスト目的でのみパブリックになっていることが他の開発者に示されます。

テスト ディレクトリを作成する

Android プロジェクトでは、test ディレクトリにローカルテストが作成されます。

test ディレクトリを作成します。

  1. [Project] タブで、ビューを [Project] に変更します。

a6b5eade0103eca9.png

  1. src ディレクトリを右クリックします。

d6bfede787910341.png

  1. [New] > [Directory] を選択します。

a457c469e7058234.png

  1. [New Directory] ウィンドウで [test/java] を選択します。

bd2c2ef635f0a392.png

  1. キーボードで Return キーまたは Enter キーを押します。[Project] タブに test ディレクトリが表示されます。

d07872d354d8aa92.png

test ディレクトリのパッケージ構造は、アプリコードが配置されている main ディレクトリと同じにする必要があります。つまり、アプリコードが main > java > com > example > tiptime パッケージに作成されている場合、ローカルテストは test > java > com > example > tiptime に作成されるようにします。

このパッケージ構造を test ディレクトリに作成します。

  1. test/java ディレクトリを右クリックし、[New] > [Package] を選択します。

99fcf5ff6cda7b57.png

  1. [New Package] ウィンドウに「com.example.tiptime」と入力します。

6223d2f5664ca35f.png

テストクラスを作成する

test パッケージの準備が整ったので、テストを作成します。まず、テストクラスを作成します。

  1. [Project] タブで、[app] > [src] > [test] をクリックし、test ディレクトリの横にある展開矢印 d4706c21930a1ef3.png をクリックします。
  2. com.example.tiptime ディレクトリを右クリックして、[New] > [Kotlin Class/File] を選択します。

5e9d46922b587fdc.png

  1. クラス名として「TipCalculatorTests」と入力します。

9260eb95d7aa6095.png

テストを作成する

前述のように、ローカルテストはアプリ内の小規模なコードをテストするために使用します。Tip Time アプリのメインの機能はチップの計算であるため、チップ計算ロジックが正しく動作するかどうかを確認するローカルテストが必要です。

そのためには、アプリコードと同様に、calculateTip() 関数を直接呼び出す必要があります。次に、関数によって返された値が、関数に渡した値から期待される値と一致するかどうかを確認します。

自動テストを作成するにあたり、知っておくべきことがいくつかあります。ローカルテストとインストルメンテーション テストの両方に当てはまるコンセプトのリストを以下に示します。最初は抽象的に思えるかもしれませんが、この Codelab を終える頃にはよく理解できるはずです。

  • メソッドの形式で自動テストを記述します。
  • メソッドに @Test アノテーションを付けます。これにより、コンパイラはメソッドがテストメソッドであることを認識し、そのメソッドをテストとして実行します。
  • 何をテストするか、どのような結果が期待されるかを明確に示す名前であることを確認します。
  • テストメソッドでは、通常のアプリメソッドのようなロジックは使用しません。何かを実装する方法を気にする必要はありません。特定の入力から期待される出力を厳密にチェックします。言い換えると、テストメソッドは、アプリの UI またはロジックが正しく動作することをアサートする命令のセットのみを実行します。これがどういう意味かは後でわかるので、まだ理解できなくてもかまいません。今のところは、テストコードは見慣れたアプリコードとはかなり異なるということだけを覚えておいてください。
  • 一般的に、テストはアサーションで終了します。アサーションは、特定の条件が満たされたことを確認するために使用されます。アサーションは、名前に assert が含まれるメソッド呼び出しの形式で行われます。たとえば、Android のテストでは assertTrue() アサーションがよく使用されます。アサーション ステートメントはほとんどのテストで使用されますが、実際のアプリコードではめったに使用されません。

テストを作成します。

  1. 10 ドルの請求額に対する 20% のチップ計算をテストするメソッドを作成します。この計算の期待される結果は 2 ドルです。
import org.junit.Test

class TipCalculatorTests {

   @Test
   fun calculateTip_20PercentNoRoundup() {

   }
}

アプリコードの MainActivity.kt ファイルの calculateTip() メソッドには、3 つのパラメータが必要でした。それは、請求額、チップの割合、計算結果を四捨五入するかどうかを示すフラグです。

fun calculateTip(amount: Double, tipPercent: Double, roundUp: Boolean)

テストからこのメソッドを呼び出すときは、アプリコード内でこのメソッドを呼び出すときと同様に、これらのパラメータを渡す必要があります。

  1. calculateTip_20PercentNoRoundup() メソッド内で、次の 2 つの定数変数を作成します。10.00 の値に設定された amount 変数と、20.00 の値に設定された tipPercent 変数。
val amount = 10.00
val tipPercent = 20.00
  1. アプリコードの MainActivity.kt ファイルで、以下のコードを確認します。チップの金額は、デバイスのロケールに基づいて書式設定されます。

MainActivity.kt

...
NumberFormat.getCurrencyInstance().format(tip)
...

テストで予想されるチップ金額を確認するときは、同じフォーマットを使用する必要があります。

  1. NumberFormat.getCurrencyInstance().format(2) に設定された expectedTip 変数を作成します。

expectedTip 変数は、後で calculateTip() メソッドの結果と比較されます。テストでは、このメソッドが正しく動作することを確認します。最後のステップでは、amount 変数の値を 10.00 に設定し、tipPercent 変数の値を 20.00 に設定します。10 の 20% は 2 であるため、expectedTip 変数は値が 2 のフォーマットされた通貨に設定されます。calculateTip() メソッドはフォーマットされた String 値を返すことを思い出してください。

  1. amount 変数と tipPercent 変数を指定して calculateTip() メソッドを呼び出し、端数の切り上げに関する false 引数を渡します。

この例では、期待される結果に端数が含まれないので、切り上げを気にする必要はありません。

  1. メソッド呼び出しの結果を actualTip 定数変数に格納します。

これまでのところ、このテストの作成は、アプリコード内の通常のメソッドの作成と大きな違いはありません。ただし、テスト対象のメソッドからの戻り値があるため、アサーションを使用してその値が正しいかどうかを判定する必要があります。

一般的に、アサーションの作成は自動テストの最終目標です。アサーションは、アプリコードではほとんど使用されません。この例では、actualTip 変数が expectedTip 変数と等しいことを確認します。そのためには、JUnit ライブラリの assertEquals() メソッドを使用できます。

assertEquals() メソッドは、2 つのパラメータ(期待される値と実際の値)を受け取ります。それらの値が等しければ、アサーションとテストは合格です。等しくなければ、アサーションとテストは失敗です。

  1. この assertEquals() メソッドを呼び出し、パラメータとして expectedTip 変数と actualTip 変数を渡します。
import org.junit.Assert.assertEquals
import org.junit.Test
import java.text.NumberFormat

class TipCalculatorTests {

    @Test
    fun calculateTip_20PercentNoRoundup() {
        val amount = 10.00
        val tipPercent = 20.00
        val expectedTip = NumberFormat.getCurrencyInstance().format(2)
        val actualTip = calculateTip(amount = amount, tipPercent = tipPercent, false)
        assertEquals(expectedTip, actualTip)
    }
}

テストを実行する

いよいよテストを実行するときが来ました。

クラス名とテスト関数の行番号の横のガターに、矢印が表示されていることにお気づきでしょうか。この矢印をクリックすると、テストを実行できます。メソッドの横の矢印をクリックすると、そのテストメソッドだけが実行されます。クラス内に複数のテストメソッドがある場合は、クラスの横の矢印をクリックすると、そのクラス内のすべてのテストメソッドを実行できます。

722bf5c7600bc004.png

テストを実行します。

  • クラス宣言の横の矢印をクリックし、次に [Run 'TipCalculatorTests'] をクリックします。

a294e77a57b0bb0a.png

次の画面が表示されます。

  • [Run] ペインの下部に、出力が表示されます。

c97b205fef4da587.png

5. インストルメンテーション テストを作成する

インストルメンテーション ディレクトリを作成する

インストルメンテーション ディレクトリは、ローカルテスト ディレクトリの場合と同じように作成されます。

  1. src ディレクトリを右クリックし、[New] > [Directory] を選択します。

309ea2bf7ad664e2.png

  1. [New Directory] ウィンドウで [androidTest/java] を選択します。

7ad7d6bba44effcc.png

  1. キーボードで Return キーまたは Enter キーを押します。[Project] タブに androidTest ディレクトリが表示されます。

bd0a1ed4d803e426.png

main ディレクトリと test ディレクトリのパッケージ構造が同じであるように、androidTest ディレクトリのパッケージ構造も同じである必要があります。

  1. androidTest/java フォルダを右クリックして、[New] > [Package] を選択します。
  2. [New Package] ウィンドウに「com.example.tiptime」と入力します。
  3. キーボードで Return キーまたは Enter キーを押します。androidTest ディレクトリの完全なパッケージ構造が [Project] タブに表示されます。

テストクラスを作成する

Android プロジェクトでは、インストルメンテーション テスト ディレクトリは androidTest ディレクトリとして指定されます。

インストルメンテーション テストを作成するには、ローカルテストの作成と同じプロセスを繰り返す必要がありますが、今回は androidTest ディレクトリ内に作成します。

テストクラスを作成します。

  1. プロジェクト ペインの androidTest ディレクトリに移動します。
  2. tiptime ディレクトリが表示されるまで、各ディレクトリの横の展開矢印 cf54f6c094aa8fa3.png をクリックします。

14674cbab3cba3e2.png

  1. tiptime ディレクトリを右クリックして、[New] > [Kotlin Class/File] を選択します。
  2. クラス名として「TipUITests」と入力します。

acd0c385ae834a16.png

テストを作成する

インストルメンテーション テストのコードは、ローカルテストのコードとは大きく異なります。

インストルメンテーション テストでは、アプリの実際のインスタンスとその UI をテストします。したがって、Tip Time アプリのコードを作成したときに MainActivity.kt ファイルの onCreate() メソッドでコンテンツを設定したのと同様の方法で、UI コンテンツを設定する必要があります。この作業は、Compose で作成されたアプリのすべてのインストルメンテーション テストを作成する前に行う必要があります。

Tip Time アプリのテストでは、続いて UI コンポーネントを操作する手順を記述し、UI を使用してチップ計算プロセスをテストできるようにします。インストルメンテーション テストのコンセプトは、最初は抽象的に思えるかもしれませんが、心配は無用です。このプロセスについては、次のステップでカバーします。

テストを作成します。

  1. composeTestRule 変数を作成して createComposeRule() メソッドの結果に設定し、これに Rule アノテーションを付けます。
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TipUITests {

   @get:Rule
   val composeTestRule = createComposeRule()
}
  1. calculate_20_percent_tip() メソッドを作成し、これに @Test アノテーションを付けます。
import org.junit.Test

@Test
fun calculate_20_percent_tip() {
}

コンパイラは、androidTest ディレクトリにある @Test アノテーション付きのメソッドがインストルメンテーション テスト用であり、test ディレクトリにある @Test アノテーション付きのメソッドがローカルテスト用であることを認識します。

  1. 関数本体で、composeTestRule.setContent() 関数を呼び出します。これにより、composeTestRule の UI コンテンツが設定されます。
  2. 関数のラムダ本体で、TipTimeLayout() 関数を呼び出すラムダ本体を指定して TipTimeTheme() 関数を呼び出します。
import com.example.tiptime.ui.theme.TipTimeTheme

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
           TipTimeLayout()
        }
    }
}

作成されたコードは、MainActivity.kt ファイルの onCreate() メソッドでコンテンツを設定する際に記述したコードと似たものになります。UI コンテンツの設定が完了したら、アプリの UI コンポーネントを操作する手順を記述できます。このアプリでは、請求額とチップの割合の入力に基づいて、アプリが正しいチップの値を表示するかどうかをテストする必要があります。

  1. UI コンポーネントは、composeTestRule によりノードとしてアクセスできます。そのための一般的な方法は、onNodeWithText() メソッドを使用して、特定のテキストを含むノードにアクセスすることです。onNodeWithText() メソッドを使用して、請求額が入力される TextField コンポーザブルにアクセスします。
import androidx.compose.ui.test.onNodeWithText

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
}

次に、performTextInput() メソッドを呼び出して、TextField コンポーザブルに入力したいテキストを渡します。

  1. 請求額の TextField に値 10 を入力します。
import androidx.compose.ui.test.performTextInput

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
    composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
}
  1. 同様に、チップの割合の OutlinedTextField に値 20 を入力します。
@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            TipTimeLayout()
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
}

すべての TextField コンポーザブルに値を入力すると、アプリの画面の下部にある Text コンポーザブルにチップが表示されます。

上記の TextField コンポーザブルに値を入力するようテストに指示したので、アサーションを使用して、Text コンポーザブルが正しいチップを表示することを確認する必要があります。

Compose を使用したインストルメンテーション テストでは、UI コンポーネントでアサーションを直接呼び出すことができます。いくつかのアサーションを使用できますが、ここでは assertExists() メソッドを使用します。チップ金額を表示する Text コンポーザブルは、Tip Amount: $2.00 を表示することが期待されます。

  1. このテキストを含むノードが存在するというアサーションを作成します。
import java.text.NumberFormat

@Test
fun calculate_20_percent_tip() {
    composeTestRule.setContent {
        TipTimeTheme {
            Surface (modifier = Modifier.fillMaxSize()){
                TipTimeLayout()
            }
        }
    }
   composeTestRule.onNodeWithText("Bill Amount")
      .performTextInput("10")
   composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
   val expectedTip = NumberFormat.getCurrencyInstance().format(2)
   composeTestRule.onNodeWithText("Tip Amount: $expectedTip").assertExists(
      "No node with this text was found."
   )
}

テストを実行する

インストルメンテーション テストの実行プロセスは、ローカルテストの実行プロセスと同じです。個々の宣言の横にあるガターの矢印をクリックして、個々のテストまたはテストクラス全体を実行します。

ad45b3e8730f9bf2.png

  • クラス宣言の横の矢印をクリックします。デバイスまたはエミュレータで、テストの実行を確認できます。テストが終了すると、次の画像のような出力が表示されます。

bfd75ec0a8a98999.png

6. 解答コードを取得する

または、GitHub リポジトリのクローンを作成してコードを入手することもできます。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout test_solution

7. まとめ

お疲れさまでした。この演習では、Android の初めての自動テストを作成しました。テストは、ソフトウェア品質管理の重要な要素です。今後も Android アプリを開発する場合は、開発プロセス全体を通してアプリが適切に動作することを確認するために、アプリ機能とともにテストも作成してください。

まとめ

  • 自動テストの概要
  • 自動テストが重要である理由
  • ローカルテストとインストルメンテーション テストの違い
  • 自動テストの作成に関する基本的なベスト プラクティス
  • Android プロジェクト内でローカルテストとインストルメンテーション テストのクラスが配置される場所と配置すべき場所
  • テストメソッドを作成する方法
  • ローカルテストとインストルメンテーション テストのクラスを作成する方法
  • ローカルテストとインストルメンテーション テストでアサーションを作成する方法
  • テストルールを使用する方法
  • ComposeTestRule を使用してテストでアプリを起動する方法
  • インストルメンテーション テストでコンポーザブルを操作する方法
  • テストを実行する方法