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 がテストクラスの新しいインスタンスを作成します。
- コード カバレッジは、アプリを構成するクラス、メソッド、コード行を適切にテストしているかどうかを判断するために重要な役割を果たします。