DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

AndroidX x JUnit5でUIテストを書こう

こんにちは。SWETチームの@zhailujiaです。 今回はAndroidX x Junit5を使ったUIテストの書き方を紹介して行きたいと思います。

背景

  • JUnit 5はJUnit 4と比べて複数な新機能があって使いたいところですが、現時点GoogleはAndroidのJUnit 5テストをまだ公式対応していません。
  • android-junit5は、AndoridプロジェクトでもJUnit5の使用を可能にするサードパーティ製のGradle Pluginです。
  • 今年4月に、android-junit5から新しいInstrumentationサポート用のInstrumentation 1.0.0Libraryをリリースしました。
  • これによりAndroidXのActivityScenarioなどのAPIとJunit5を組み合わせたUIテストが遂に書けるになりました。

システム環境

System Requments

  • Android Gradle Plugin 3.2.0 or higher
  • Gradle 4.7 or higher
  • Java 8
  • Android 8.0/API 26/Oreo or higher

今回動作確認したバージョン

  • 開発環境
    • AndroidStudio 3.4.1
    • Android Gradle Plugin 3.2.1
    • Gradle 4.10.1
    • Java SE 1.8.0_181
  • 検証端末
    • Android 9.0/API 28

android-junit5プラグインの導入

  • 下記のサンプルはInstrumentation Tests on JUnit 5に必要最小限の設定を記述しています。
  • Android Instrumentation Test on JUnit 4、Local Unit Test on JUnit 5も同一プロジェクトに設定して共存できますが、今回は割愛します。

1. プロジェクトルートのbuild.gradle

buildscript {  
    dependencies {
        // 1) Add android-junit5 plugin to project
        classpath "de.mannodermaus.gradle.plugins:android-junit5:1.4.2.0"
    }
}

2. モジュールのbuild.gradle

android {
    defaultConfig {
        // Use AndroidX test runner
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        // 2) Connect JUnit 5 to the runner
        testInstrumentationRunnerArgument "runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder"
    }  

    // 3) Java 8 is required, add this even if minSdkVersion is 26 or above
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }  

    // 4) JUnit 5 will bundle in files with identical paths; exclude them
    packagingOptions {
        exclude("META-INF/LICENSE*")
    }
}  

// 5) (Optional) If use ParameterizedTest ArgumentsProvider, this is required for set to recompile with "-jvm-target 1.8"
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}  

dependencies {
    // 6) Jupiter API
    androidTestImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"   
    // 7) (Optional) Jupiter Parameters
    androidTestImplementation "org.junit.jupiter:junit-jupiter-params:5.4.2"

    // 8) JUnit 5 instrumentation companion libraries
    androidTestImplementation "de.mannodermaus.junit5:android-test-core:1.0.0"
    androidTestRuntimeOnly "de.mannodermaus.junit5:android-test-runner:1.0.0"

    androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
}

Instrumentation Testテストを書こう

ProjectソースはAndroid-Junit5/Instrumeny/Sample/から引用しています、ご参照ください。

AndroidX ActivityScenario on JUnit 5

1. ActivityScenarioExtensionから実現できるJUint5 Instrumentationテスト

  • android-junit5はActivityScenarioExtensionを提供することで、ActivityScenario APIを使用可能になりました。
  • ActivityScenarioExtensionはAndroidX x JUnit4のActivityScenarioRuleのJUnit 5版です。
  • Android Test x JUnit4のActivityTestRuleは廃止予定なので、そのJUnit 5版@ActivityTestも廃止になりました。
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import de.mannodermaus.junit5.ActivityScenarioExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

/* Android Oreo/8.0/API 26 以下はスキップされる */
class ExampleInstrumentedTest {
  
    /*
     Instrumentation Testのために、Activityを起動します
     @JvmField annotation
       - kotlinで書いているなら必要
     @RegisterExtension annotation
       - ここが最重要、ActivityScenarioのExtension
       - JUnit 4の@RuleのJUnit 5版
     ActivityScenarioExtension.launch<activityClass>
       - ActivityScenarioRuleの代わりに使う
       - activityClassを起動します
    */
    @JvmField 
    @RegisterExtension 
    val scenarioExtension = ActivityScenarioExtension.launch<ActivityOne>()

    @Test
    fun testExample() {
        onView(withId(R.id.textView)).check(matches(withText("0")))
    }
}

2. ActivityScenarioExtensionとActivityScenarioの関係

  • ActivityScenarioExtensionからActivityScenarioを取得できる
    @Test
    fun testExampleUseScenario() {
        // use scenarioExtension to get ActivityScenario
        val scenario = scenarioExtension.scenario
        // then get Activity just same as JUnit 4
        scenario.onActivity { activity ->
            assertEquals(0, activity.getClickCount())
        }
    }

3. Use ActivityScenario with "JUnit 5 Parameter Resolution" feature

  • ActivityScenarioExtensionはJUnit5のParameterResolverを継承しているので、Parameter Resolution機能に対応している。
  • そのため、ActivityScenarioは、直接テストケースのパラメータとして使うことができる。
    // A scenario can be passed into a test method directly, when using the ActivityScenarioExtension
    @Test
    fun testExampleWithParameter(scenario: ActivityScenario<ActivityOne>) {
        scenario.onActivity {
            assertEquals(0, it.getClickCount())
        }
    }

JUnit 5をもっと使ってみよう

Android JUnit 4とJUnit 5のAnnotationマッピング

JUnit 4 JUnit 5
@org.junit.Test @org.junit.jupiter.api.Test
@RunWith deprecated
@Ignore @Disabled
n/a @DisplayName
n/a @Nested
n/a @ParameterizedTest + <Source>
n/a @RepeatedTest(int)
n/a @TestInstance
Assert.assertXXX Assertions.assertXXX

これらのうち、JUnit5で特徴的な、次の2つの機能について、具体的な使い方を紹介します

  • テストの構造化に使う@Nested@Display
  • パラメトライズドテストに使う@RepeatedTest@ParameterizedTest

1. NestedとDisplayName

  • すでにJUnit 5のunit testで馴染まれているNestedとDisplayNameアノテーションはinstrumentation testでも使えます。
  • テストの構造化、日本語の表示を実現できます。
    @Nested
    @DisplayName("Test Group")
    inner class NestedTests {
        @Test
        @DisplayName("テストサンプル")
        fun testExample() {
            onView(withId(R.id.textView)).check(matches(withText("0")))
        }
    }

テスト実行結果

f:id:swet-blog:20190605122005p:plain:w300:left

2. RepeatedTest

  • 指定した回数で繰り返しテストを実行できます。
  • RepetitionInfoからcurrentRepetition(現在の実行回数)、totalRepetitions(実行すべき総回数)を取得できます。
  • RepeatedTestはcustom DisplayNameに対応している。
    // repeat this test 3 times
    // combines a custom display name pattern of each repetition via the name attribute
    @RepeatedTest(value = 3, name = "{currentRepetition}/{totalRepetitions} clickCount={currentRepetition}, expected=''{currentRepetition}''")
    fun repeatedTestExample(repetitionInfo: RepetitionInfo) {
        val count = repetitionInfo.currentRepetition

        for (i in 0 until count) {
            onView(withId(R.id.button)).perform(click())
        }
        onView(withId(R.id.textView)).check(matches(withText(count.toString())))
    }

テスト実行結果

f:id:swet-blog:20190605125839p:plain:w480:left

3. ParameterizedTest

  • ParameterizedTestは異なる引数でテストを複数回実行できます。
  • テストデータは引数ソースSources of Argumentsアノテーションで宣言する。
    • ソースアノテーションは複数種類ありますが、今回は展開せず@ArgumentsSourceを使う例を挙げます。
  • テストメソッドのパラメータとしてデータを受け取ります。
  • ParameterizedTestもcustom DisplayNameに対応している(今回は省略)。
    // define enum for show action description on test results
    enum class Action(val rawValue :ViewAction) {
        Click(click()),
        DoubleClick(doubleClick()),
        LongClick(longClick())
    }
    
    // define custom arguments class as an implementation of ArgumentsProvider
    // ArgumentsProvider must be declared as either a top-level class or as a static nested class
    private class ButtonTestArguments : ArgumentsProvider {
        override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of(
            Arguments.arguments(Action.Click, 1),
            Arguments.arguments(Action.DoubleClick, 2),
            Arguments.arguments(Action.LongClick, 1)
        )
    }

    @ParameterizedTest
    // @ArgumentsSource is one of the Sources of Arguments Annotation
    @ArgumentsSource(ButtonTestArguments::class)
    // get parameters(action,expected) from the Sources of Arguments named ButtonTestArguments
    // can use both of parameterized arguments(action, expected) and extensions parameter resolution(scenario) at the same time
    fun parameterizedTestExample(action: Action , expected: Int, scenario: ActivityScenario<ActivityOne>) {
        onView(withId(R.id.button)).perform(action.rawValue)

        scenario.onActivity {
            assertEquals(expected, it.getClickCount())
        }
    }

テスト実行結果

f:id:swet-blog:20190605125018p:plain:w480:left

パラメタライズドテストは以下のような効果が考えられます

  • RepeatedTestとParameterizedTestを活用して、重複するテストコード、テストケースを削減できます。
  • argumentsを利用してにテストのGiven-When-Thenをペアに設定ことで可読性が上がります。
  • Esspressoだけで画面上複数のビューの操作する時、テストコードがダラダラ長くなりがち部分も、arguments化して綺麗にまとまります。

終わりに

  • 今回紹介したJUnit 5を使えるAndroid UIテストの書き方は分かりやすいですか? ご興味があれば試してみてください。
  • android-junit5はまだリリースされたばかりなので、RepeatedTestとParameterizedTestの動作にはいくつか注意点がありますが、今回は割愛させていただきます。
  • 紹介した内容以外にもAndroidX Fragment Test、Android Test OrchestratorやJUnit5 Test Instance Lifecycleなど一歩進んだ使い方もあります。
  • 今回はこれで終わりにしようと思いますので、今度チャンスがあればまた一緒にAndroidX x JUnit5のさらなる活用を使ってみましょう。

参考リスト

謝辞

  • @marcelschnellさんと共同開発者たちが素晴らしいプラグインを開発してくださって深く感謝しています。
  • 同じくSWETグループの外山さん田熊さんからAndroidテストとJUnit 5に関して貴重な意見と経験を教えていただきました。ありがとうございます。