Help us understand the problem. What is going on with this article?

Androidテスト全書のまとめ

More than 1 year has passed since last update.

ユニットテストをまったく書けないので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...を選択
既にテストが存在すれば表示される

スクリーンショット 2018-11-12 23.15.33.png

作成ダイアログの矢印をONにしてOK

スクリーンショット 2018-11-12 20.27.23.png

ディレクトリ選択ダイアログ
test配下に作成するようにする。

スクリーンショット 2018-11-12 20.26.26.png

ユニットテスト実装

アサーション

JUnit4では org.junit.Assert.assertThat()メソッドを使って
assetThat(実測値, マッチャー(期待値))という形式でアサーションを行う。

マッチャー

assetThatメソッドの第2引数に「期待する状態がどのような条件か」を定義したマッチャーを指定する。
JUnit4には Hamcrest というマッチャーライブラリが組み込まれており、豊富なマッチャーを利用できる。

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の依存関係を追加する。

app/build.gradle
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

テストダブルを使った依存コンポーネントの差し替え

Mockito でテストダブルを便利に扱う

ikemura23
Androidアプリエンジニアです。 マイブームはFlutterとAWS
http://banbara-studio.hatenablog.com/
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。東京(三軒茶屋)/京都(四条烏丸)/札幌/大阪/福岡に展開中!Twitterで情報配信中https://twitter.com/yumemiinc
http://www.yumemi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした