ユニットテストをまったく書けないのでAndroidテスト全書という本を買った。
1章から2章の途中までしか読みきれてないため、そこまでのまとめ。
随時追記していく。
テストの種類
ユニットテスト
クラスごとにメソッドが期待通りの値を返すか、プロパティが適切に変化するか、を保証されるために書かれる
Local Unit Test
実機はエミュレータを使用せず開発マシンから直接高速に実行できるテスト。
実行速度は早い。
Instrumented Unit Test
実機やエミュレータを使ってユニットテストを実行する手法。
実際にユーザーが触れる形式に変換してAndroid上で実行する。
実行速度が遅い。
UIテスト
ユーザーが実際にアプリのUIを操作したときに画面が正しく反応できるか検証するためのテスト。
テストのピラミッド
ピラミッドの下層であればあるほどテストサイズが小さくて量が多く、頻繁に実行すべきである
引用元
Fundamentals of Testing | Android Developers https://developer.android.com/training/testing/fundamentals
テストの作り方
1. Android Studioでテスト対象クラスの上で Command+Shift+T
Create New Test...
を選択
既にテストが存在すれば表示される
作成ダイアログの矢印をONにしてOK
ディレクトリ選択ダイアログ
test
配下に作成するようにする。
ユニットテスト実装
アサーション
JUnit4では org.junit.Assert.assertThat()
メソッドを使って
assetThat(実測値, マッチャー(期待値))という形式でアサーションを行う。
マッチャー
assetThatメソッドの第2引数に「期待する状態がどのような条件か」を定義したマッチャーを指定する。
JUnit4には Hamcrest というマッチャーライブラリが組み込まれており、豊富なマッチャーを利用できる。
その他マッチャー
- より大きい:
greaterThan()
- Collectionが期待値を含んでいる:
hasItem()
実装したユニットテスト
Android-Kotlin-Lab/InputCheckerTest.kt at unit_test · banbara23/Android-Kotlin-Lab · GitHub
テストをコマンドラインで実行
./gradlew test
より詳しい使い方
https://d.android.com/studio/test/command-line
テストケースごとの初期化・後処理
JUnit4では初期化処理の共通化のために arg.junit.Before
アノテーションが用意されている。
@Before
アノテーションで修飾されたメソッドは各テストケースが走る直前に毎回実行される。
テストケースの例外を検証
メソッドがnull
を受け取ったらIllegalAugumentExceptionをthrowする事を検証する場合、
JUnit4では@Test(expected = 例外クラス)
として対象メソッドが意図通り例外を発生させたか検証できる。
@Test(expected = IllegalArgumentException::class)
fun isValid_givenNull_throwsIllegalArgumentException() {
target.isValid(null)
}
メソッドが例外をthrowされなかったらテストケースは失敗と扱われる。
また、expectedオプションは例外を1種類しか区別できない。
テストケースのスキップ
テストケースにarg.junit.Ignore
アノテーションをつけると一時的にスキップする。
@Ignore("仮実装なので一時的にスキップ") //理由を書くと分かりやすい
@Test
fun temporarilySkipThisTest() {
//略
}
@Ignoreがついたテストケースは長時間放置されるのは望ましくない、一時的に利用するのみにとどめる。
JUnit4のテストランナー
JUnit4ではテストランナー(Test Runner)を使ってそれぞれのテストケースを収集・実行し、結果をユーザーに知らせる。
JUnit4ではテストクラスに対してテストランナーを切り替える事ができる。
arg.junit.runner.RunWith
アノテーションにテストランナークラスを指定する。
@RunWith(JUnit4::class) // ←JUnit4標準のテストランナー、省略したらこのランナーが使われる
class InputCheckerTest {
}
他にもテストランナーがいくつかある
- AndroidJUnitRunner
- Instrumented Testを実行するためのAndroid標準のテストランナー
- MockitoJUnitTestRunner
- Mockitoで使うテストランナー
- RoboletricTestRunner
JUnit4のテストルール
JUnit4でテストの実行方法を制御するために、テストルールという仕組みが提供されている。
テストルールは、各テストケースの実行前後にフックしてテストのメタ情報にアクセスできるので、@Beforeや@Afterアノテーションで行っていた初期化や終了処理をプラグインのように切り出して再利用できる。
AssertJを使ったアサーション
AssertJとは、メソッドチェーンを利用したJava向けのアサーションライブラリ。
Hamcrestよりも自然言語に近く読みやすいアサーションを書くことができる。
AssertJを使うには
app/build.gradleにAssertJの依存関係を追加する。
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.10.0'
}
AssertJを使用したテスト
import org.assertj.core.api.Assertions.assertThat
@Test
fun isValid_givenAlphaNumeric_returnTrue() {
val actual = target.isValid("abc123")
assertThat(actual).isTrue() // <=AssertJで書き換えた
}
文字列のアサーション
"TOKYO"という文字列を様々な条件で検証する。
AssertJでは複数のアサーションメソッドチェーンでいくつも並べて書くことができる。
assertThat("TOKYO")
.`as`("TEXT CHECK TOKYO")
.isEqualTo("TOKYO")
.isEqualToIgnoringCase("tokyo")
.isNotEqualTo("KYOTO")
.isNotBlank()
.startsWith("TO")
.endsWith("YO")
.contains("OKY")
.matches("\[A-Z\]{5}")
.isInstanceOf(String::class.java)
メソッドチェーンで並べて書いた場合はAND条件となり、上から順に評価されて偽になったところで即座にテストケースは失敗となる。
もしアサーションのうちいずれかが失敗しても最後までテストケースを実行したい場合はSoftAssertions
を利用する。
SoftAssertions().apply {
assertThat("TOKYO")
.`as`("TEXT CHECK TOKYO")
.isEqualTo("TOKYO")
.isEqualToIgnoringCase("tokyo")
.isNotEqualTo("KYOTO")
.isNotBlank()
.startsWith("TO")
.endsWith("YO")
.contains("OKY")
.matches("\[A-Z\]{5}")
.isInstanceOf(String::class.java)
}.assertAll()
数値のアサーション
@Test
fun assertJ_numeric() {
assertThat(3.14159)
.isNotZero() //ゼロではなく
.isNotNegative() //負数ではなく
.isGreaterThan(3.0) //3より大きく
.isLessThanOrEqualTo(4.0) //4以下
.isBetween(3.0, 3.2) //3.0から3.2の範囲内
.isCloseTo(Math.PI, within(0.001)) //Math.PIで定義された円周率の定数から誤差0.001以内である
}
}
コレクションのアサーション
AssertJで「要素が含まれているか」や「その順番はどうか」といった検証を直感的に扱うことができる。
val target = listOf("Giants", "Dodgers", "Athletics")
assertThat(target)
.hasSize(3) //要素の個数を検証
.contains("Dodgers") //要素がリストに含まれるかどうか
.containsOnly("Athletics", "Dodgers", "Giants") //順不同で等価な要素のみが含まれているか
.containsExactly("Giants", "Dodgers", "Athletics") //等価な要素のみが同じ順序で同じ組み合わせで重複なしに含まれるか
.doesNotContain("Padres") //含んでいないか
コレクションのフィルタリング
AssertJではコレクションから結果データを絞り込んだり(フィルタリング)、複雑なオブジェクトから調べたいプロパティだけをピックアップしたりといった検証に便利な機能が提供されている。
data class BallTeam(val name: String, val city: String, val stadium: String)
val target = listOf(
BallTeam("Giants", "San Francisco", "AT&T Park"),
BallTeam("Dodgers", "Los Angels", "Dodger Stadium"),
BallTeam("Angels", "Los Angels", "Angel Stadium"),
BallTeam("Athletics", "Oakland", "Oakland Coliseum"),
BallTeam("Padres", "San Diego", "Petco Park")
)
assertThat(target)
.filteredOn { team -> team.city.startsWith("San") } //Sanで始まり
.filteredOn { team -> team.city.endsWith(("Francisco")) } //Franciscoで終わるものをフィルタリング
.extracting("name", String::class.java) //name:Stringプロパティだけを取り出す
.containsExactly("Giants") //extractingで取り出されたname要素はGiantsである
assertThat(target)
.filteredOn { team -> team.city == "Los Angels" } //cityがLos Angelsのものをフィルタリング
.extracting("name", "stadium") //nameとstadiumだけ取り出す
.containsExactly(
tuple("Dodgers", "Dodger Stadium"), //tupleでこのプロパティだけをもった一時的な型として扱って比較する
tuple("Angels", "Angel Stadium")
)
AssertJの例外検証
AssertJではJUnit4標準の@Test(expected=例外クラス)
よりも例外を簡単かつ詳細に検証できる。
@Test
fun assetJ_exception() {
assertThatExceptionOfType(RuntimeException::class.java) //キャッチしたい例外のタイプ
.isThrownBy { functionsMyThrow() } //例外をだす可能性のあるメソッドを指定
.withMessage("Aborted!") //詳細メッセージの検証
.withNoCause() //この例外がほかの例外経由で送出されていないことを検証
}
private fun functionsMyThrow() {
throw java.lang.RuntimeException("Aborted!")
}
AssertJまとめ
詳しくは公式をチェック
http://joel-costigliola.github.io/assertj/
余談:次世代アサーションライブラリの覇者
Googleが開発するアサーションライブラリ Truth
はAssertJに似たインタフェースをもち、読みやすくKotlinとの相性もいいので細流よく候補の一つとなっている。
これ?
GitHub - google/truth: Fluent assertions for Java