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に変更してくれるとします。
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
}
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
}
- Buttonをクリック
- 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()
}
- ActivityController生成
- ActivityControllerのLifecycleを変更し、
get()
でActivity戻り - text確認
- 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
- Android - 振動、Vibrator、VibrationEffectの例
- Android - TabLayoutの実装方法(+ ViewPager2)
- Android - PackageManagerにPackage情報を取得する
- Android - ACTION_BOOT_COMPLETEDイベント受信
- Android - FusedLocationProviderClientに位置情報を取得する
- Android - GPS、Network位置情報を取得する(LocationManager)
- Android - Foreground Service実行
- Android - 時間、日付、変更イベント受信
- Android - currentTimeMillis()、elapsedRealtime()、uptimeMillis()
- Android-PowerManager WakeLock
- Android - ファイル入出力の例(Read、Write、内部、外部ストレージ)
- Android - Screen On / Offイベントの受信、状態確認
- Android - 他のアプリのServiceにバインド
- Android - Handler vs Executor
- Android - Darkmode有効にする方法
- Android - hasSystemFeature()、サポートされているFeature確認
- Android - アプリの権限を確認(Permission check)
- Android - インストールされてアプリリストをインポートする
- Android App Shortcuts実装
- Android - ContentProviderを実装、および例
- Android - AIDLを利用して、Remote Serviceの実装
- Android - Uri、Scheme、SSP(Scheme Specific Part)説明
- Android - アプリのインストール、削除、イベントダウンロード(BroadcastReceiverインテントを受け取る)
- Android - SharedPreferencesに簡単なデータを保存する方法
- Android - AlarmManagerにアラームを登録する方法、および例
- Android - Quick SettingsにCustom Tile追加する方法(kotlin)
- Android - Broadcast Receiver登録およびイベントの受信方法
- Android - Runtime permissionリクエスト方法と例(kotlin)
- Android - ネットワーク(WIFI)の接続状態を確認し、変更の検出
- Mockito - static、final methodをmockingする方法
- Andriod - カスタムパーミッションを定義する方法
- RobolectricにUnit Testを作成する(kotlin)
- Android Mockitoのテストコードを作成する(kotlin)
- Android - Handlerの使用方法、および例
- Android - IntentService使用方法
- Android - JobIntentService使用方法