🧪 Android Glance の単体テストの備忘録
Glance とは
Android のホーム画面に表示する Widget を Jetpack compose で作成できるライブラリ。
Glance 1.1.0 で安定版リリースされて、Google I/O 2025 でも触れられていたので、今後も開発が進められそう。
↓Google I/O 2025 の Widget の部分。
Glance 単体テスト
以前は UiState の単体テストを記述することでなんとかデグレチェックを担保していたが、どのような表示になっているかまでは難しかった。
Glance のテストツールを使うことで Jetpack compose の単体テストのように UI Automator を使用せずに記述できる。
環境
- Android Studio Android Studio Meerkat | 2024.3.1 Patch 2
- Kotlin 2.1.21
- Glance 1.1.1
- Robolectric 4.14.1
設定
公式サイトでは下記のような記述で設定できると書かれている。
今回は LocalContext が必要で Robolectric も使用するので、差分を追加。
android {
...
+ testOptions {
+ unitTests {
+ // For Robolectric
+ isIncludeAndroidResources = true
+ }
+ }
}
dependencies {
// Other Glance and Compose runtime dependencies.
...
testImplementation("androidx.glance:glance-testing:1.1.1")
testImplementation("androidx.glance:glance-appwidget-testing:1.1.1")
testImplementation("org.robolectric:robolectric:4.14.1")
...
// You may include additional dependencies, such as Robolectric, if your test
// needs to set a LocalContext.
+ testImplementation("androidx.test.ext:junit-ktx:1.2.1")
}
テストコード
import androidx.test.core.app.ApplicationProvider
...
@RunWith(RobolectricTestRunner::class)
class WidgetTest {
private val context: Context = ApplicationProvider.getApplicationContext() // ①
@Test
fun itemEmpty_shouldShowLoadingIndicator() = runGlanceAppWidgetUnitTest {
provideComposable {
CompositionLocalProvider(
// NOTE: Scaffold の内部で LocalContext が使用されているので必要 ②
LocalContext provides context,
) {
WidgetComposable(
items = emptyList(),
onItemClick = {},
)
}
}
onNode(hasTestTag("loading_indicator")) // ③
.assertExists()
}
}
① context 取得
androidx.test.ext:junit-ktx の androidx.test.core.app.ApplicationProvider をインポートしてそこから context を取得。
context を使用するので @RunWith(RobolectricTestRunner::class) か @RunWith(AndroidJUnit4::class) などを忘れずに設定。
② LocalContext を上書き
CompositionLocalProvider を使って①で取得した context に上書き。
これがないと LocalContext を使用した Scaffold などのコンポーネントなどがあると下記のようなエラーになる。
java.lang.IllegalStateException: No default context
余談:LocalContext を使用していない Scaffold はいつリリースされるのか
main ブランチの Scaffold は LocalContext を使用していないけど、Glance 1.1.1 の Scaffold はまだ LocalContext を使用している。使用していない版はいつリリースされるのだろうか。
3163676: [glance] Fix resource check for corner radius | https://android-review.googlesource.com/c/platform/frameworks/support/+/3163676
Submitted Jul 10, 2024
③ Glance のテストタグ
通常の Compose のテストタグと少し違う。
1. テストタグの設定方法
GlanceModifier には通常の Compose にある Modifier#testTag がないので sementics で設定する。
// 通常の Compose
Modifier.testTag(tag = "loading_indicator"),
// Glance
GlanceModifier.semantics { testTag = "loading_indicator" }
2. ルートのコンポーネントで testTagsAsResourceId が不要
当然と言えば当然なんだが、UI Automator を使用しているわけではないので、不要。
むしろ、設定するプロパティがないので、UI Automator を使用してテストすることができない?
Modifier.semantics { testTagsAsResourceId = true }
Jetpack Compose × UiAutomator の相互運用で詰まった時の備忘録はこっち。
文字列の取得
これは Glance に限らず、context から取得することもできる。
@Test
fun isErrorTrue_shouldShowErrorMessage() = runGlanceAppWidgetUnitTest {
val errorMessage = context.getString(R.string.widget_error)
provideComposable {
CompositionLocalProvider(
// NOTE: Scaffold の内部で LocalContext が使用されているので必要
LocalContext provides context,
) {
WidgetComposable(
items = emptyList(),
isError = true,
onItemClick = {},
)
}
}
onNode(hasText(errorMessage))
.assertExists()
}
余談
文字列取得は通常の Compose のように下記のような util があると便利
@Composable
@ReadOnlyComposable
fun stringResource(@StringRes id: Int): String {
val resources = LocalContext.current.resources
return resources.getString(id)
}
@Composable
@ReadOnlyComposable
fun stringResource(@StringRes id: Int, vararg formatArgs: Any): String {
val resources = LocalContext.current.resources
return resources.getString(id, *formatArgs)
}
参考
StringResources.android.kt
Discussion