1. 始める前に
この Codelab では、ViewModel
コンポーネントをテストする単体テストの作成方法について説明します。Unscramble ゲームアプリの単体テストを追加します。Unscramble アプリは、スクランブルされた単語を推測し、正解するとポイントを獲得できる、楽しい単語ゲームです。次の画像は、アプリのプレビューを示しています。
「自動テストを作成する」Codelab では、自動テストの概要と重要性について学習しました。また、単体テストの実装方法についても学習しました。
学習した内容は以下のとおりです。
- 自動テストは、別のコードの正確さを検証するコードです。
- テストは、アプリの開発における重要なプロセスです。アプリに対して一貫性のあるテストを実施することで、アプリの公開前に、機能の動作、使いやすさを検証できます。
- 単体テストでは、関数、クラス、プロパティをテストできます。
- ローカル単体テストは、ワークステーションで実行されます。つまり、Android デバイスやエミュレータを必要とせずに、開発環境で実行されます。言い換えると、ローカルテストはパソコンで実行できます。
先に進む前に、「自動テストを作成する」と「Compose での ViewModel と状態」の Codelab を完了してください。
前提条件
- 関数、ラムダ、ステートレス コンポーザブルなど、Kotlin に関する知識
- Jetpack Compose でレイアウトを作成する方法に関する基本的な知識
- マテリアル デザインに関する基本的な知識
- ViewModel を実装する方法に関する基本的な知識
学習内容
- アプリ モジュールの
build.gradle.kts
ファイルに単体テストの依存関係を追加する方法 - 単体テストを実装するためのテスト戦略を作成する方法
- JUnit4 を使用して単体テストを作成し、テスト インスタンスのライフサイクルを理解する方法
- コード カバレッジを実行、分析、改善する方法
作成するアプリの概要
- Unscramble ゲームアプリの単体テスト
必要なもの
- Android Studio の最新バージョン
スターター コードを取得する
まず、スターター コードをダウンロードします。
または、GitHub リポジトリのクローンを作成してコードを入手することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout viewmodel
コードは Unscramble
GitHub リポジトリで確認できます。
2. スターター コードの概要
ユニット 2 では、次の図に示すように、src フォルダの下にある test ソースセットに単体テストコードを配置することについて学習しました。
スターター コードには次のファイルがあります。
WordsData.kt
: このファイルには、テストに使用する単語のリストと、スクランブルされた単語からスクランブルされていない単語を取得するgetUnscrambledWord()
ヘルパー関数が含まれています。このファイルを変更する必要はありません。
3. テストの依存関係を追加する
この Codelab では、JUnit フレームワークを使用して単体テストを作成します。このフレームワークを使用するには、アプリ モジュールの build.gradle.kts
ファイルに依存関係として追加する必要があります。
implementation
構成を使用して、アプリに必要な依存関係を指定します。たとえばアプリで ViewModel
ライブラリを使用するには、次のコード スニペットに示すように、依存関係を androidx.lifecycle:lifecycle-viewmodel-compose
に追加する必要があります。
dependencies {
...
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
これで、このライブラリをアプリのソースコードで使用できるようになりました。Android Studio で、生成されたアプリ パッケージ(APK)ファイルにライブラリを追加できます。しかし、単体テストコードを APK ファイルの一部にすることは望ましくありません。テストコードはユーザーが使用する機能を追加するものではありませんし、APK のサイズにも影響します。テストコードに必要な依存関係についても同様であり、それぞれ別にしておく必要があります。そのためには、testImplementation
構成を使用します。こうすることで、構成がアプリコードではなくローカルのテスト ソースコードに適用されることを示します。
プロジェクトに依存関係を追加するには、build.gradle.kts
ファイルの dependencies ブロックで、implementation
や testImplementation
などの依存関係構成を指定します。それぞれの依存関係構成は、依存関係の使用方法について Gradle にさまざまな指示を与えます。
依存関係を追加するには:
- [Project] ペインの
app
ディレクトリにある、app
モジュールのbuild.gradle.kts
ファイルを開きます。
- ファイル内で、
dependencies{}
ブロックが見つかるまで下にスクロールします。junit
のtestImplementation
構成ファイルを使用して依存関係を追加します。
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- 次のスクリーンショットに示すように、build.gradle.kts ファイルの上部にある通知バーで [Sync Now] をクリックし、インポートとビルドを終了します。
部品構成表(BOM)の作成
Compose ライブラリのバージョン管理には、Compose BOM の使用をおすすめします。BOM を使用すると、BOM のバージョンのみを指定して、すべての Compose ライブラリ バージョンを管理できます。
app
モジュールの build.gradle.kts
ファイルの依存関係セクションをご覧ください。
// No need to copy over
// This is part of starter code
dependencies {
// Import the Compose BOM
implementation (platform("androidx.compose:compose-bom:2023.06.01"))
...
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
...
}
以下のことを確認してください。
- Compose ライブラリのバージョン番号が指定されていません。
- BOM が
implementation platform("androidx.compose:compose-bom:2023.06.01")
を使用してインポートされています。
これは、BOM 自体に、さまざまな Compose ライブラリが連携して適切に機能するよう、それぞれの最新の安定版ライブラリへのリンクが含まれているためです。アプリで BOM を使用するときに、Compose ライブラリの依存関係自体にバージョンを追加する必要はありません。BOM バージョンを更新すると、使用しているすべてのライブラリが自動的に新しいバージョンに更新されます。
BOM を Compose テスト ライブラリ(インストルメンテーション テスト)で使用するには、androidTestImplementation platform("androidx.compose:compose-bom:xxxx.xx.xx")
をインポートする必要があります。次に示すように、変数を作成して implementation
と androidTestImplementation
に再利用することができます。
// Example, not need to copy over
dependencies {
// Import the Compose BOM
implementation(platform("androidx.compose:compose-bom:2023.06.01"))
implementation("androidx.compose.material:material")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
// ...
androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
これでテストの依存関係をアプリに追加し、BOM について学習できたので、単体テストを追加する準備が整いました。
4. テスト戦略
適切なテスト戦略は、コードのさまざまなパスと境界をカバーすることを中心に展開されます。非常に基本的なレベルでは、成功パス、エラーパス、境界ケースという 3 つのシナリオにテストを分類できます。
- 成功パス: 成功パスのテスト(「ハッピーパス テスト」ともいいます)は、ポジティブ フローの機能のテストに重点を置きます。ポジティブ フローとは、例外またはエラー状態がないフローのことです。エラーパスや境界ケースのシナリオとは異なり、成功パスのシナリオは、アプリの想定どおりの動作に重点を置くため、シナリオの網羅的なリストを簡単に作成できます。
Unscramble アプリの成功パスの例としては、ユーザーが正しい単語を入力して [Submit] ボタンをクリックしたときに、スコア、単語数、スクランブルされた単語が正しく更新されることが挙げられます。
- エラーパス: エラーパスのテストは、ネガティブ フローの機能のテストに重点を起きます。つまり、エラー状態や無効なユーザー入力にアプリがどのように応答するかをチェックします。想定どおりの動作が達成されなかったときに生じる可能性のある結果は数多くあるため、考えられるすべてのエラーフローを判断することは非常に困難です。
一般的に、考えられるすべてのエラーパスを列挙し、それらに対するテストを作成して、さまざまなシナリオを発見しながら単体テストを進化させ続けることをおすすめします。
Unscramble アプリのエラーパスの例としては、ユーザーが間違った単語を入力して [Submit] ボタンをクリックしたときに、エラー メッセージが表示され、スコアと単語数が更新されないことが挙げられます。
- 境界ケース: 境界ケースは、アプリの境界条件のテストに重点を置きます。Unscramble アプリの境界では、アプリが読み込むときの UI 状態と、ユーザーが最大数の単語をプレイした後の UI 状態を確認します。
これらのカテゴリに関するテストシナリオを作成し、テスト計画のガイドラインとして使用できます。
テストを作成する
一般的に、適切な単体テストには次の 4 つの特性があります。
- 範囲が限定的: コードの一部など、特定のユニットをテストすることに重点が置かれています。コードの一部とは多くの場合、クラスまたはメソッドです。テストは範囲を狭くし、コードの複数の部分を同時に検証するのではなく、コードの各部分の正確さを検証することに重点を置くべきです。
- 理解しやすい: 簡潔で、コードを読んだときに理解しやすいものにします。デベロッパーがひと目で、テストの背後にある意図をすぐに理解できるようにしましょう。
- 確定的: 合格か不合格かの判定に一貫性があります。コードを変更せずに何度テストを実行しても、同じ結果が得られる必要があります。コードを変更していないにもかかわらず、あるときは不合格になり、またあるときは合格になるなど、テストが不安定であってはなりません。
- 自己完結: 人間による操作もセットアップも必要とせず、単独で動作します。
成功パス
成功パスの単体テストを作成するには、GameViewModel
のインスタンスが初期化されている前提で、updateUserGuess()
メソッドが正しい推測単語で呼び出された後に checkUserGuess()
メソッドが呼び出されたとき、次のようになることをアサートする必要があります。
- 正しい推測が
updateUserGuess()
メソッドに渡される。 checkUserGuess()
が呼び出される。score
とisGuessedWordWrong
ステータスの値が正しく更新される。
次の手順を実施してテストを作成します。
- 次のスクリーンショットに示すように、テスト ソースセットの下に新しいパッケージ
com.example.android.unscramble.ui.test
を作成し、ファイルを追加します。
GameViewModel
クラスの単体テストを作成するには、クラスのメソッドを呼び出して状態を検証できるように、クラスのインスタンスが必要となります。
GameViewModelTest
クラスの本体で、viewModel
プロパティを宣言し、GameViewModel
クラスのインスタンスを代入します。
class GameViewModelTest {
private val viewModel = GameViewModel()
}
- 成功パスの単体テストを作成するには、
gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset()
関数を作成し、@Test
アノテーションを付けます。
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
}
}
- 以下をインポートします。
import org.junit.Test
プレーヤーの正しい単語を viewModel.updateUserGuess()
メソッドに渡すには、GameUiState
のスクランブルされた単語からスクランブルされていない正しい単語を取得する必要があります。そのためには、まず現在のゲームの UI 状態を取得します。
- 関数本体で
currentGameUiState
変数を作成し、viewModel.uiState.value
を代入します。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
}
- プレーヤーの正しい推測を取得するには
getUnscrambledWord()
関数を使用します。この関数はcurrentGameUiState.currentScrambledWord
を引数として受け取り、スクランブルされていない単語を返します。この戻り値をcorrectPlayerWord
という新しい読み取り専用変数に格納し、getUnscrambledWord()
関数から返された値を代入します。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- 推測した単語が正しいかどうかを検証するには、
viewModel.updateUserGuess()
メソッドの呼び出しを追加し、引数としてcorrectPlayerWord
変数を渡します。次に、viewModel.checkUserGuess()
メソッドの呼び出しを追加して、推測を検証します。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
これで、ゲームの状態が想定どおりであることをアサートする準備が整いました。
viewModel.uiState
プロパティの値からGameUiState
クラスのインスタンスを取得し、currentGameUiState
変数に格納します。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
}
- 推測した単語が正しいこととスコアが更新されたことを確認するには、
assertFalse()
関数を使用してcurrentGameUiState.isGuessedWordWrong
プロパティがfalse
であることを検証し、assertEquals()
関数を使用してcurrentGameUiState.score
プロパティの値が20
に等しいことを検証します。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
// Assert that checkUserGuess() method updates isGuessedWordWrong is updated correctly.
assertFalse(currentGameUiState.isGuessedWordWrong)
// Assert that score is updated correctly.
assertEquals(20, currentGameUiState.score)
}
- 以下をインポートします。
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
- 値
20
を理解しやすくし、再利用できるようにするには、コンパニオン オブジェクトを作成して、SCORE_AFTER_FIRST_CORRECT_ANSWER
というprivate
定数に20
を代入します。新しく作成した定数でテストを更新します。
class GameViewModelTest {
...
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
...
// Assert that score is updated correctly.
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
}
companion object {
private const val SCORE_AFTER_FIRST_CORRECT_ANSWER = SCORE_INCREASE
}
}
- テストを実行します。
次のスクリーンショットに示すように、すべてのアサーションが有効であるため、テストは合格するはずです。
エラーパス
エラーパスの単体テストを作成するには、間違った単語が引数として viewModel.updateUserGuess()
メソッドに渡され、viewModel.checkUserGuess()
メソッドが呼び出されたら、次のようになることをアサートする必要があります。
currentGameUiState.score
プロパティの値は変更されない。- 推測が間違っているため、
currentGameUiState.isGuessedWordWrong
プロパティの値はtrue
に設定される。
次の手順を実施してテストを作成します。
GameViewModelTest
クラスの本体で、gameViewModel_IncorrectGuess_ErrorFlagSet()
関数を作成し、@Test
アノテーションを付けます。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
}
incorrectPlayerWord
変数を定義し、単語のリストに存在しない"and"
値を代入します。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
}
viewModel.updateUserGuess()
メソッドの呼び出しを追加し、引数としてincorrectPlayerWord
変数を渡します。viewModel.checkUserGuess()
メソッドの呼び出しを追加して、推測を検証します。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
}
currentGameUiState
変数を追加し、viewModel.uiState.value
状態の値を代入します。- アサーション関数を使用して、
currentGameUiState.score
プロパティの値が0
であり、currentGameUiState.isGuessedWordWrong
プロパティの値がtrue
に設定されていることをアサートします。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
val currentGameUiState = viewModel.uiState.value
// Assert that score is unchanged
assertEquals(0, currentGameUiState.score)
// Assert that checkUserGuess() method updates isGuessedWordWrong correctly
assertTrue(currentGameUiState.isGuessedWordWrong)
}
- 以下をインポートします。
import org.junit.Assert.assertTrue
- テストを実行して、合格することを確認します。
境界ケース
UI の初期状態をテストするには、GameViewModel
クラスの単体テストを作成する必要があります。このテストは、GameViewModel
が初期化されたとき、次の事項が真であることをアサートする必要があります。
currentWordCount
プロパティが1
に設定されている。score
プロパティが0
に設定されている。isGuessedWordWrong
プロパティがfalse
に設定されている。isGameOver
プロパティがfalse
に設定されている。
次の手順でテストを追加します。
gameViewModel_Initialization_FirstWordLoaded()
メソッドを作成し、@Test
アノテーションを付けます。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
}
viewModel.uiState.value
プロパティにアクセスしてGameUiState
クラスの初期インスタンスを取得し、新しい読み取り専用変数gameUiState
に代入します。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
}
- プレーヤーの正しい単語を取得するには
getUnscrambledWord()
関数を使用します。この関数はgameUiState.currentScrambledWord
の単語を受け取り、スクランブルされていない単語を返します。戻り値を、unScrambledWord
という新しい読み取り専用変数に代入します。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
}
- 状態が正しいことを検証するには、
assertTrue()
関数を追加して、currentWordCount
プロパティが1
に設定され、score
プロパティが0
に設定されていることをアサートします。 assertFalse()
関数を追加して、isGuessedWordWrong
プロパティがfalse
であることと、isGameOver
プロパティがfalse
に設定されていることを検証します。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
// Assert that current word is scrambled.
assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
// Assert that current word count is set to 1.
assertTrue(gameUiState.currentWordCount == 1)
// Assert that initially the score is 0.
assertTrue(gameUiState.score == 0)
// Assert that the wrong word guessed is false.
assertFalse(gameUiState.isGuessedWordWrong)
// Assert that game is not over.
assertFalse(gameUiState.isGameOver)
}
- 以下をインポートします。
import org.junit.Assert.assertNotEquals
- テストを実行して、合格することを確認します。
もう 1 つの境界ケースは、ユーザーがすべての単語を推測した後の UI 状態をテストすることです。ユーザーがすべての単語を正しく推測したとき、次の事項が真であることをアサートする必要があります。
- スコアが最新である。
currentGameUiState.currentWordCount
プロパティがMAX_NO_OF_WORDS
定数の値と等しい。currentGameUiState.isGameOver
プロパティがtrue
に設定されている。
次の手順でテストを追加します。
gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()
メソッドを作成し、@Test
アノテーションを付けます。このメソッドでexpectedScore
変数を作成し、0
を代入します。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
}
- 初期状態を取得するには、
currentGameUiState
変数を追加し、viewModel.uiState.value
プロパティの値を代入します。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
}
- プレーヤーの正しい単語を取得するには
getUnscrambledWord()
関数を使用します。この関数はcurrentGameUiState.currentScrambledWord
の単語を受け取り、スクランブルされていない単語を返します。この戻り値をcorrectPlayerWord
という新しい読み取り専用変数に格納し、getUnscrambledWord()
関数から返された値を代入します。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- ユーザーがすべての答えを推測しているかどうかをテストするには、
repeat
ブロックを使用して、viewModel.updateUserGuess()
メソッドとviewModel.checkUserGuess()
メソッドの実行をMAX_NO_OF_WORDS
回繰り返します。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
}
}
repeat
ブロックで、SCORE_INCREASE
定数の値をexpectedScore
変数に追加して、正解するたびにスコアが増加することをアサートします。viewModel.updateUserGuess()
メソッドの呼び出しを追加し、引数としてcorrectPlayerWord
変数を渡します。viewModel.checkUserGuess()
メソッドの呼び出しを追加して、ユーザーの推測の確認をトリガーします。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
}
- 現在のプレーヤーの単語を更新し、
getUnscrambledWord()
関数を使用します。この関数は、currentGameUiState.currentScrambledWord
を引数として受け取り、スクランブルされていない単語を返します。この戻り値をcorrectPlayerWord.
という新しい読み取り専用変数に格納します。状態が正しいことを確認するには、assertEquals()
関数を追加して、currentGameUiState.score
プロパティの値がexpectedScore
変数の値と等しいことを確認します。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// Assert that after each correct answer, score is updated correctly.
assertEquals(expectedScore, currentGameUiState.score)
}
}
assertEquals()
関数を追加して、currentGameUiState.currentWordCount
プロパティの値がMAX_NO_OF_WORDS
定数の値と等しいことと、currentGameUiState.isGameOver
プロパティの値がtrue
に設定されていることをアサートします。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// Assert that after each correct answer, score is updated correctly.
assertEquals(expectedScore, currentGameUiState.score)
}
// Assert that after all questions are answered, the current word count is up-to-date.
assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
// Assert that after 10 questions are answered, the game is over.
assertTrue(currentGameUiState.isGameOver)
}
- 以下をインポートします。
import com.example.unscramble.data.MAX_NO_OF_WORDS
- テストを実行して、合格することを確認します。
テスト インスタンスのライフサイクルの概要
テストで viewModel
を初期化する方法をよく見ると、すべてのテストで使用されているにもかかわらず、viewModel
が 1 回しか初期化されていないことがわかります。次のコード スニペットは、viewModel
プロパティの定義を示しています。
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
...
}
...
}
次のような疑問を抱かれるかもしれません。
- すべてのテストで
viewModel
の同じインスタンスが再利用されるということなのか。 - 何か問題が生じるのか。たとえば、
gameViewModel_Initialization_FirstWordLoaded
テストメソッドがgameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset
テストメソッドの後に実行された場合はどうなるか。初期化テストは失敗するか。
どちらの疑問に対しても、答えは「いいえ」です。テストメソッドは、テスト インスタンスの状態が可変であることによる予期しない副作用を避けるために、独立して実行されます。デフォルトでは、各テストメソッドが実行される前に JUnit がテストクラスの新しいインスタンスを作成します。
GameViewModelTest
クラスにはここまででテストメソッドが 4 つあるため、GameViewModelTest
は 4 回インスタンス化します。各インスタンスにはそれぞれ viewModel
プロパティのコピーがあります。そのため、テスト実行の順序は重要ではありません。
5. コード カバレッジについて
コード カバレッジは、アプリを構成するクラス、メソッド、コード行を適切にテストしているかどうかを判断するために重要な役割を果たします。
Android Studio にはローカル単体テストのためのテスト カバレッジ ツールが用意されており、単体テストがカバーするアプリコードの割合と領域が追跡されます。
Android Studio でカバレッジを指定してテストを実行する
カバレッジを指定してテストを実行するには:
- プロジェクト ペインで
GameViewModelTest.kt
ファイルを右クリックし、 [Run 'GameViewModelTest' with Coverage] を選択します。
- テスト実行が完了したら、右側のカバレッジ パネルで [Flatten Packages] オプションをクリックします。
- 次の図に示すように、
com.example.android.unscramble.ui
パッケージがあります。
- パッケージ名
com.example.android.unscramble.ui
をダブルクリックすると、GameViewModel
のカバレッジが次の図のように表示されます。
テストレポートを分析する
次の図に示すレポートは、2 つの部分に分かれています。
- 単体テストがカバーするメソッドの割合: 図の例では、これまでに作成したテストが 8 つのメソッドのうち 7 つをカバーしています。これはメソッド全体の 87% に相当します。
- 単体テストがカバーする行の割合: 図の例では、作成したテストが 41 行のうち 39 行をテストしています。これはコード行の 95% に相当します。
このレポートは、これまでに作成した単体テストでコードの特定の部分が見逃されていることを示唆しています。どの部分が見逃されているのかを判断する手順は次のとおりです。
- [GameViewModel] をダブルクリックします。
Android Studio で、GameViewModel.kt
ファイルがウィンドウの左側で色分けされて表示されます。明るい緑色は、そのコード行がカバーされていることを示します。
GameViewModel
を下にスクロールすると、いくつかの行が薄いピンク色でマークされていることがわかります。この色は、そのコード行が単体テストでカバーされていないことを示します。
カバレッジを改善する
カバレッジを改善するには、見逃しているパスをカバーするテストを作成する必要があります。テストを追加して、ユーザーが単語をスキップしたとき、次の事項が真であることをアサートする必要があります。
currentGameUiState.score
プロパティは変更されない。- 次のコード スニペットに示すように、
currentGameUiState.currentWordCount
プロパティは 1 ずつ増加する。
カバレッジを改善する準備として、次のテストメソッドを GameViewModelTest
クラスに追加します。
@Test
fun gameViewModel_WordSkipped_ScoreUnchangedAndWordCountIncreased() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
val lastWordCount = currentGameUiState.currentWordCount
viewModel.skipWord()
currentGameUiState = viewModel.uiState.value
// Assert that score remains unchanged after word is skipped.
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
// Assert that word count is increased by 1 after word is skipped.
assertEquals(lastWordCount + 1, currentGameUiState.currentWordCount)
}
次の手順を実施してカバレッジを再実行します。
GameViewModelTest.kt
ファイルを右クリックし、メニューから [Run ‘GameViewModelTest' with Coverage] を選択します。- ビルドが成功したら、もう一度 GameViewModel 要素に移動し、カバレッジの割合が 100% であることを確認します。次の図に、最終的なカバレッジ レポートを示します。
GameViewModel.kt
ファイルに移動して下にスクロールし、前に見逃されていたパスがカバーされているかどうかを確認します。
アプリコードのコード カバレッジを実行、分析、改善する方法を学習しました。
コード カバレッジの割合が高ければ、アプリコードの質が高いということになるのでしょうか。そうではありません。コード カバレッジは、単体テストでカバー(実行)されたコードの割合を示すもので、コードが検証されたことを示しているわけではありません。単体テストのコードからすべてのアサーションを削除してコード カバレッジを実行しても、カバレッジは 100% と表示されます。
カバレッジが高いことは、テストが正しく設計されていることや、アプリの動作が検証されていることを示すわけではありません。作成したテストに、テストするクラスの動作を検証するアサーションがあることを確認する必要があります。また、アプリ全体でテスト カバレッジ 100% を実現するように単体テストを作成する必要はありません。アクティビティなど、アプリのコードの一部は UI テストでテストする必要があります。
ただし、カバレッジが低いということは、コードの大部分がまったくテストされていないことを意味します。コード カバレッジは、コードの品質を測定するツールではなく、テストで実行されなかったコードの部分を見つけるツールとして使用してください。
6. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、以下の git コマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout main
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
解答コードを確認する場合は、GitHub で表示します。
7. まとめ
お疲れさまでした。テスト戦略を定義する方法を学習し、Unscramble アプリの ViewModel
と StateFlow
をテストする単体テストを実装しました。今後も Android アプリを開発する場合は、開発プロセス全体を通してアプリが適切に動作することを確認するために、アプリ機能とともにテストも作成するようにしてください。
まとめ
testImplementation
構成を使用して、依存関係がアプリコードではなく、ローカルのテスト ソースコードに適用されることを示します。- テストを、成功パス、エラーパス、境界ケースという 3 つのシナリオに分類するようにします。
- 適切な単体テストには、少なくとも 4 つの特性(範囲が限定的、理解しやすい、確定的、自己完結)があります。
- テストメソッドは、テスト インスタンスの状態が可変であることによる予期しない副作用を避けるために、独立して実行されます。
- デフォルトでは、各テストメソッドが実行される前に JUnit がテストクラスの新しいインスタンスを作成します。
- コード カバレッジは、アプリを構成するクラス、メソッド、コード行を適切にテストしているかどうかを判断するために重要な役割を果たします。