単体テストを作成する

1. 始める前に

これまでの Codelab では、Android Studio でプロジェクトを作成する方法、アプリ用に UI をカスタマイズするために XML を変更する方法、機能を追加するためにビジネス ロジックを変更する方法について学びました。この Codelab では、テストが重要な理由に焦点を当て、単体テストについて詳しく説明します。単体テストの概要や作成方法を確認できます。

前提条件

  • Android Studio でプロジェクトを作成したことがある。
  • Android Studio でコードを記述した経験がある。

学習内容

  • テストが重要な理由
  • 単体テストの概要
  • 単体テストを作成して実行する方法

必要なもの

  • Android Studio がインストールされているパソコン
  • このパスウェイの前の Codelab で作成したプロジェクト

2. はじめに

Android コードを作成したので、次はテストコードです。まず、テストの理念を学び、次に Android プロジェクトで自動生成されるテストについて詳しく確認し、最後に Dice Roller アプリ用に独自のテストを作成します。このレッスンではさまざまな題材を扱っていますが、恐れることはありません。テストには長時間を要し、かなりの練習が必要になるため、これには時間をかけてください。すぐに理解できなくても、がっかりする必要はありません。

テストが重要な理由

最初は、アプリにテストはあまり必要ないように思うかもしれません。アプリが小さく限られた機能しかない場合は、手動でテストし、すべてが正しく動作しているかどうかを判断することは簡単です。しかし、アプリが大きくなると、手動テストは自動テストを作成するよりもはるかに多くの労力を必要とします。さらに、プロフェッショナル レベルのアプリに取り組み始めると、大規模なユーザーベースがある場合はテストが重要になります。その場合、さまざまなバージョンの Android を搭載したさまざまな種類のデバイスを考慮する必要があります。規模が大きくなると、最終的には、手動テストより自動テストの方が大幅に速く使用シナリオの大部分をカバーできるようになります。新しいコードをリリースする前にテストを実行すると、予期しない動作をするアプリがリリースされないよう、既存のコードに変更を加えることができます。自動テストは、人がデバイスを直接操作して実施する手動テストとは対照的に、ソフトウェアを通じて実行されるテストであることに留意してください。自動テストと手動テストは、ユーザーがプロダクトを快適に利用できるようにするために重要な役割を果たします。ただし、自動テストの方が正確であり、チームの生産性を最適化できます。これは、人が実行する必要がなく、手動テストよりもはるかに速く実行できるためです。

単体テストの詳細

この Codelab では、単体テストに焦点を当てます。インストルメンテーション テストについては、後ほど説明します。まず、Android Studio で Android アプリを作成する際に生成されるテストについて確認します。また、テストを実行してテストコードの記述に慣れるハンズオンも行います。

前のパスウェイでは、テスト用のソースファイルがどこにあるのか確認しました。単体テストは、常に test ディレクトリにあります。

f02b380da4e8f661.png

  1. app/build.gradle ファイルを開き、依存関係を確認します。一部の依存関係は testImplementation および androidTestImplementation とマークされています。それぞれ、単体テストとインストルメンテーション テストに対応しています。以下に注目してください。

app/build.gradle

testImplementation 'junit:junit:4.12'

単体テストを稼働させる JUnit ライブラリです。コードをテストとしてマークし、アプリコードをテストできるようにコンパイルして実行します。

  1. test ディレクトリで、ExampleUnitTest.kt ファイルを開きます。

次のようなサンプル単体テストが表示されるはずです。

ExampleUnitTest.kt

class ExampleUnitTest {
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
   }
}

Dice Roller アプリにコードを追加しましたが、テストは作成していないでしょう。そのため、Android Studio によって自動的に作成される汎用コード以外、何もありません。これは任意のテストであり、デベロッパーが記述することが期待される、関連性の高いテストのプレースホルダとして機能します。現在、このコードブロックは 2 + 2 = 4 であることをテストするだけです。もちろん、これは常に true です。何が起きているのか、詳しく見てみましょう。

  • テスト関数には、まず org.junit.test ライブラリからインポートした @Test アノテーションを付ける必要があります。アノテーションは、コードの一部に対するメタデータタグと考えることができ、コードのコンパイル方法を変更できます。この場合、@Test アノテーションは、次のメソッドがテストであることをコンパイラに知らせ、そのように動作させます。

アノテーションの後に、関数宣言(この場合は addition_isCorrect() 関数)があります。この関数の中で、assertEquals() 関数は、期待値がビジネス ロジックから取得した実際値に等しいということについてアサーションを行います。アサーション メソッドは単体テストの最終目標です。最終的には、コードから得られる結果が特定の状態にあるというアサーションを行います。結果の状態が期待される状態と一致する場合、テストは合格となります。結果の状態が期待される状態と一致しない場合、テストは失敗します。この場合、コードが 2 つの値を比較しているため、assertEquals() メソッドは 2 つのパラメータ(期待値と実際値)を取ります。その名のとおり、期待値とは、特定の結果がどうなることが期待されるかを示すものです(この場合は 4)。実際値は、実際のコードの結果を表します。一般に、これはアプリ自体のコードの一部をテストします。この場合は、コードの任意の部分のみをテストします(2 + 2 など)。それでは、このテストを実行して何が起こるか確認してみましょう。

Android Studio でテストを実行する方法は多数ありますが、それについては後で詳しく説明します。ここではシンプルにします。

  1. addition_isCorrect メソッド宣言の横にある矢印をクリックし、[Run ‘ExampleUnitTest.addition_isCorrect'] を選択します。

78c943e851a33644.png

これは陽性テストと呼ばれるテストです。つまり、肯定的なアサーションを行います(例: 2 + 2 は 4 と等しい)。代わりに、否定的なアサーションを行う陰性テストを作成することもできます(例: 2 + 2 は 5 と等しくない)。

[Run] ペインは、次のスクリーンショットのように表示されます。

190df0c8ff787233.png

テストが成功したことを示す各種の指標、つまり緑色のチェックマークと合格したテストの数が表示されています。

aa7d361d8e4826ef.png

  1. テストを変更して、失敗した場合どのようになるかを確認してみます。2 + 22 + 3 に変更し、もう一度テストを実行します。生成されたコードでのテストは、テストの仕組みを理解するためにのみ行っていることに留意してください。これらの変更は、Dice Roller の機能とは何の関連もありません。

ExampleUnitTest.kt

class ExampleUnitTest {
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 3)
   }
}

残りを実行すると、次のスクリーンショットのように表示されます。

751ac8089cf4c47c.png

赤色のテキストはテストの失敗を示しています。テスト結果のメニューでアイテムをクリックすると、テストが失敗した理由を示すエラー メッセージが表示されます。

163708373e651ecc.png

この場合、メッセージは、4 という結果が期待されていたが実際値は 5 であったためにアサーションが失敗したということを示しています。実際値を 2 + 3 に変更しましたが、期待値は 4 のままであったため、これは妥当です。テストが失敗した行も表示されます。この場合は 15 行目であり、ExampleUnitTest.kt:15 として示されています。

  1. 念のため、期待値を 4 から 5 に変更してもう一度テストを実行してみましょう。期待値が該当するコードの実際の結果と一致するため、テストは合格するはずです。

3.初めての単体テストを作成する

単体テストに慣れたところで、Dice Roller アプリに関連する独自の単体テストを作成してみましょう。

お気づきのように、Dice Roller アプリの主な機能は乱数ジェネレータに基づいています。残念ながら、乱数ジェネレータではランダムに生成された数値の結果を確認できないため、テストは非常に困難です。このテストの目的は、サイコロを振ったとき、または dice クラスの roll メソッドを呼び出したとき、適切な数値が返されることを確認することです。作成するテストでは、乱数ジェネレータの出力が、ジェネレータに指定した範囲内の数値であることだけをテストします。

  1. ExampleUnitTest.kt ファイルで、生成されたテストメソッドと import ステートメントを削除します。ファイルは次のようになります。

c06e8b402f293b5e.png

  1. generates_number() 関数を作成します。

ExampleUnitTest.kt

fun generates_number() {
}
  1. generates_number() メソッドに @Test アノテーションを付けます。@Test を呼び出そうとすると、テキストが赤色になります。これは、このアノテーションの宣言が見つからないためであり、インポートする必要があります。Control+Enter(Mac の場合は Options+Return)を押すと自動的に行われます。

コード行をクリックすると、インポートするためのプロンプトが表示されます。

bbe5791b9565588c.png

または、import org.junit.Test ファイルをコピーして、パッケージ名の後、クラス宣言の前に貼り付けることもできます。コードは次のようになります。

9a94c2bdf84adb61.png

  1. Dice オブジェクトのインスタンスを作成します。

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
}
  1. 次に、このインスタンスで roll() メソッドを呼び出して、返された値を格納します。

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
   val rollResult = dice.roll()
}
  1. 最後に、実際のアサーションを行います。つまり、渡した面数の範囲内の値をメソッドが返したことについてのアサーションを行う必要があります。そのため、この場合は 0 より大きく、7 より小さい値である必要があります。これを実現するには、assertTrue() メソッドを使用します。assertTrue() メソッドを呼び出そうとすると、最初はテキストが赤色になります。これは、アノテーションの場合と同様に、このメソッドの宣言が見つからないためであり、インポートする必要があります。

10eea07fc21bf998.png

前述のとおり、自動的にインポートできます。ただし、今回は複数の選択肢から選べます。今回は、org.junit.Assert パッケージから選択します。

5dbfba2ba0e37ac9.png

または、テスト アノテーションの import ステートメントの後に、次のコードを貼り付けることもできます。

ExampleUnitTest.kt

import org.junit.Assert.assertTrue

コードは次のようになります。

347f792f455ae6b5.png

かっこの間にカーソルを置き、Control+P(Mac の場合は Command+P)を押すと、メソッドが取るパラメータを示すツールチップが表示されます。

865cf0ac47738e08.png

assertTrue() メソッドは、StringBoolean という 2 つのパラメータを取ります。アサーションが失敗した場合、文字列はコンソールに表示されるメッセージとなります。ブール値は条件文です。メッセージを次のように設定します。

ExampleUnitTest.kt

"The value of rollResult was not between 1 and 6"

前述のように、乱数のテストは、乱数の性質上、数値を予測できないため困難です。値が特定の範囲に収まっていることを確認することしかできません。条件パラメータを次のように設定します。

ExampleUnitTest.kt

rollResult in 1..6

コードは次のようになります。

ExampleUnitTest.kt

@Test
fun generates_number() {
   val dice = Dice(6)
   val rollResult = dice.roll()
   assertTrue("The value of rollResult was not between 1 and 6", rollResult in 1..6)
}
  1. 関数の横にある矢印をクリックし、[Run ‘ExampleUnitTest.generates_number()'] を選択します。

コードが前のコード スニペットのようであれば、テストに合格するはずです。

  1. (省略可)追加の練習として、サイコロの面数を 4 または 5 に変更し、アサーションを変更せずに、テストが失敗することを確認します。

4. 完了

学習した内容は以下のとおりです。

  • テストの重要性
  • 単体テストの概要
  • 単体テストの実行方法
  • 一般的なテスト構文
  • 単体テストの作成方法

関連リンク