Jetpack ComposeのPreviewテストで時間を参照している時にPRが暴れてしまうのを防ぐ方法
PaparazziとShowkase、TestParameterInjectorを使ってJetpack ComposeのPreviewテストを行う際に、Preview関数内で現在時刻を参照しているとpushするたびに特になんの変更をしていなくても前回のpushした時と現在時刻が違うことが原因で無駄にPreviewテストが走ってしまうという問題が発生したので、その問題に対する解決策について書いていこうと思います。
元々どうやっていたか?
元々、Github ActionsでPreviewテストを実行する条件を以下のようにしていました。
on:
pull_request:
paths:
- 'UIモジュール/**/*.kt'
- '!UIモジュール/**/src/test/**'
- '!UIモジュール/**/build.gradle.kts'
無駄にテストが実行されないように、UIを管理しているモジュールのtestとGradleファイルが変更されていた場合はPreviewテストを実行せずに、それ以外の部分が変更された場合はPreviewテストを実行するようにしていました。
UIの部分の差分を検知した場合は、自動で差分のスクショとコメントをPRに出力するようにしていました。
どういう問題が起きたか?
これで無駄にテストが実行されないと安心していたら、いきなりPRでPreviewテストが特にUIの部分を変更していないのに差分を検知してスクショをコメントで出力するようになりました。
出力されたスクショを見てみると、全て時間が違うことが原因で差分が検知されPR内でPreviewテストが暴れまくってました。
該当する関数を見てみると、全て現在時刻を参照しておりpushするたびに現在時刻が違うことが原因だということがわかりました。
どう解決したか?
Clock
というInterfaceを作成し、これをClockImpl
とFakeClock
で実装するようにすることで解決しました。
interface Clock {
val now: Instant
}
実際のクラスなどにInjectする時はこちらを使います。
class ClockImpl : Clock {
override val now: Instant
get() = 現在時刻を取得
}
Preview関数内で使用します。
class FakeClock : Clock {
// 2022-12-20T10:00:00+09:00[Asia/Tokyo]
private val fixedZonedTime by lazy {
ZonedDateTime.of(
2022,
12,
20,
10,
0,
0,
0,
ZoneId.of("Z")
)
}
override val now: Instant
get() = Instant.fromEpochSeconds(
org.threeten.bp.Clock.fixed(
fixedZonedTime.toInstant(),
fixedZonedTime.zone
).instant().epochSecond
)
}
上記のFakeClock
をCompositionLocalを使用してComposable関数内で使うようにすることで、pushするたびに現在時刻が違うということを防ぐようにすることで解決しました。
val LocalClock = compositionLocalOf<Clock> { ClockImpl() }
作成したLocalClockをComposable関数内で以下のように使います。
@Composable
fun Home() {
val now = LocalClock.current.now
.....
// CompositionLocalを使って取得したnowを使用するようにする。
}
---
@Preview
@Composable
fun HomePreview() {
val clock = FakeClock()
CompositionLocalProvider(
LocalClock provides clock
) {
Home()
}
}
これで現在時刻が原因で差分が生じることがなくなりました。
まとめ
Previewテストを導入した時は、時間が原因で無駄にテストが実行されてPRが荒れてしまうという現象は考慮できていなかったので、今回のような対応をしました。
もし、Previewテストを使用している方でPreview関数内で時間を参照しているパターンに遭遇した方は参考にしてみてください。
Discussion