RobolectricにUnit Testを作成する(kotlin)

By JS | Last updated: January 11, 2020

RobolectricはAndroidのコードをJVMでUnit testに使用するテストフレームワークです。 エミュレータや、デバイスから直接テストしていないので、速度が速くなります。 (しかし、多くの高速ないので、実際のデバイスでテストする良いことがあります。)

Android StudioのUnit testもJVMでテストを行うが、Android SDKは何の動作もしていないstub、android.jarを使用します。 このstubは何の動作もせず、nullや0を返します。 そのために必要なのは、MockitoなどでMockを作ってテストする必要があります。

一方、RobolectricはShadowという概念があります。 ShadowはMockはありませんがMockと同様に、いくつかの意図された動作を実行するテストダブルです。 RobolectricはAndroid SDKのクラスのShadowを実装しました。 このShadowを使用してテストコードを作成すると、デバイスに動作するように結果を出力することができます。 そのため、Mockを不必要に多くの作成は不要です。 また、Custom shadowが必要な場合、直接作成することもできます。

Android StudioでRobolectricを使用してUnit testを作成する方法について説明します。

この記事のコードは、すべてKotlinで作成されました。サンプルは、GitHubで確認することができます。

プロジェクトSetup

プロジェクトを作成すると、appのgradleに次のように依存性と、 testOptionsを追加します。

android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
}

dependencies {
    testImplementation 'org.robolectric:robolectric:4.3.1'
}

Android Studio 3.3未満のバージョンでは、 gradle.propertiesに以下を追加します。

android.enableUnitTestBinaryResources=true

プロジェクトを見れば、 src/の下には、次のとおりです。 ここで、 test/がJVMで動作するUnit testです。

├── androidTest
├── main
└── test
    └── java
        └── com
            └── codechacha
                └── robolectric
                    └── ExampleUnitTest.kt

ExampleUnitTest.ktファイルでは、次のようにannotationを設定します。

@RunWith(RobolectricTestRunner::class)
class ExampleUnitTest {
  ...
}

AndroidはRobolectric testコードUnitではなく、Instrumentation testでも動作することができるように努力しています。 だから次のように AndroidJUnit4::classを使用しても動作します。

@RunWith(AndroidJUnit4::class)
class ExampleUnitTest {
  ...
}

これで、プロジェクトでRobolectricにUnit testを作成することができます。

JDK設定

RobolectricはSDK API 29に対してJava 9が必要です。

[File] => [Project Structures]SDK LocationでJDKをJava9に変更してくれるとします。 java9

SDK API 29未満の場合は、Java 9に設定しなくてもコンパイルされます。

アプリの実装

テストコードを作成する前に、簡単なアプリを実装する必要があります。

次のように実装しました。

MainActivity.kt

class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "MainActivity"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "onCreate")

        mainButton.setOnClickListener {
            mainTextView.text = "Hello world!"
        }
    }

    override fun onResume() {
        Log.d(TAG, "onResume")
        super.onResume()
    }

    override fun onPause() {
        Log.d(TAG, "onPause")
        super.onPause()
    }
}

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    <Button
            android:id="@+id/mainButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"/>
    <TextView
            android:id="@+id/mainTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Not set"/>
    <TextView
            android:id="@+id/helloTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello"/>
</LinearLayout>

res/values/strings.xml

<resources>
    <string name="app_name">Robolectric</string>
    <string name="hello">Hello</string>
</resources>

res/values-ko/strings.xml

<resources>
    <string name="hello">안녕하세요</string>
</resources>

テストコードの作成(ログ出力)

さて、先ほど実装したアプリをテストするためのコードを書くことができます。

まず、 ExampleUnitTest.ktに次のように入力してください。

class ExampleUnitTest {
    @Before
    fun setup() {
        ShadowLog.stream = System.out;
    }
}

上記のコードは、 Log.d()のように、デバイスからログを出力するコードをJVMで動作にすることです。 このように設定しなければ Log.d()は、ログを出力しません。

TextViewテスト

次のコードは、ActivityのTextViewの初期値を確認するためのコードです。

@Test
fun textView_text_is_right() {
    val activity = Robolectric.setupActivity(
          MainActivity::class.java)  // 1
    assertEquals("Not set", activity.mainTextView.text)   // 2
}
  1. Robolectric.setupActivity()はActivityを初期化します。内部的にonCreate()、onResume()などが呼び出されます。

実際のActivityコードが動作しておらず、Robolectricで作成されたShadow Activityコードが動作します。 2. ActivityのTextViewを持って来textの初期値を確認します。

Shadowは、実際のコードと同様に動作するようです。

しかし、内部的にShadowがどのように実装されているかわからないので、エミュレータやデバイスでも、テスト結果が同じか?という考えが続いね。

Buttonテスト

アプリの MainActivity.ktで次のようにボタンが押されるとtextを変更するように実装しました。

mainButton.setOnClickListener {
    mainTextView.text = "Hello world!"
}

次のコードは、Buttonが押されたときtextが変更されたことを確認するテストです。

@Test
fun textView_when_click_button() {
    val activity = Robolectric.setupActivity(MainActivity::class.java)
    assertEquals("Not set", activity.mainTextView.text)

    activity.mainButton.performClick()  // 1
    assertEquals("Hello world!", activity.mainTextView.text)  // 2
}
  1. Buttonをクリック
  2. textが変更されたことを確認

ActivityController

上記のActivityを作成するときに Robolectric.setupActivity()を使用しました。 このコードは、Activityを作成たが、Lifecycleを勝手に変更することができません。

次のコードは ActivityControllerを利用して、Lifecycleを直接変更するコードです。

@Test
fun textView_text_is_right2() {
    val controller: ActivityController<MainActivity> =
            Robolectric.buildActivity(MainActivity::class.java)   // 1
    val activity = controller   // 2
            .create()
            .start()
            .resume()
            .visible()
            .get()

    assertEquals("Not set", activity.mainTextView.text)   // 3
    activity.mainButton.performClick()
    assertEquals("Hello world!", activity.mainTextView.text)

    controller    // 4
        .pause()
        .stop()
        .destroy()
}
  1. ActivityController生成
  2. ActivityControllerのLifecycleを変更し、 get()でActivity戻り
  3. text確認
  4. ActivityのLifecycleをdestroyに変更

Lifecycleが変更されるたびに、Activityの onResume()onPause()など入れログが出力されます。

Activity再実行テスト

Activityが再実行されているケースをテストしたいことができます。

次のようにActivity Lifecycleを変更して、テストコードを記述します。

@Test
fun recreatesActivity() {
    val bundle = Bundle()

    var controller: ActivityController<MainActivity> =
            Robolectric.buildActivity(MainActivity::class.java)
    controller
        .create()
        .start()
        .resume()
        .visible()
        .get()

    // Destroy the original activity
    controller
        .saveInstanceState(bundle)
        .pause()
        .stop()
        .destroy()

    // Bring up a new activity
    controller = Robolectric.buildActivity(MainActivity::class.java)
        .create(bundle)
        .start()
        .restoreInstanceState(bundle)
        .resume()
        .visible()

    val activity = controller.get()

    // ... add assertions ...
}

多言語テスト

様々なLocaleに対してテストすることができます。

次のコードのように @Config annotationにqualifiersにlocaleを設定することができます。

@Test
@Config(qualifiers = "en")
fun localizedEnglishHello() {
    val activity = Robolectric.setupActivity(MainActivity::class.java)
    assertEquals(activity.helloTextView.text.toString(), "Hello")
}

@Test
@Config(qualifiers = "ko")
fun localizedKoreanHello() {
    val activity = Robolectric.setupActivity(MainActivity::class.java)
    assertEquals(activity.helloTextView.text.toString(), "안녕하세요")
}

参考

Related Posts

codechachaCopyright ©2019 codechacha