はじめに
こんにちは。5月よりフルスタックエンジニアとしてVoicyに入社しました、たかまてぃー(@kyappamu) と申します。GW明けから始まったブログリレーの波に私も乗っからせていただきました。
今回は Jetpack Compose で Android アプリ開発をする際の Preview 表示でよく遭遇するであろう、Render issue に mockk と共に立ち向かってみたことについて書いてみたいと思います。
mockk とは
Kotlin 用のモックライブラリで、依存モジュールの振る舞いを変えたり、任意のレスポンスを返したりなど、ユニットテストでよく使われます。
発生したエラー
@Preview(showBackground = true) @Composable fun PreviewMiniPlayerView() { val miniPlayerViewModel = MiniPlayerViewModel() ・・・(略) MiniPlayerView( viewModel = miniPlayerViewModel, onTap = {}, onPlayTap = {}, onPauseTap = {}, onRewindTap = {}, onDragStart = {}, onDragEnd = {}, onDrag = {} ) }
上記プレビュー箇所で、以下のエラーが発生しました。
Render problem _layoutlib_._internal_.kotlin.UninitializedPropertyAccessException: lateinit property instance has not been initialized ・・・(略)
MiniPlayerViewModel クラスの init の処理等で voiceAudioService にアクセスしているのですが、その際にこのプロパティが初期化されていないため UninitializedPropertyAccessException エラーが発生し、結果として Preview が失敗しています。
init { viewModelScope.launch { voiceAudioService.totalElapsedTimeFlow.collect { elapsedTime -> ・・・(略) } } ・・・(略) }
private val voiceAudioService: VoiceAudioService get() { return PlayerApplication.getInstance().getAudioService() }
対応内容
そもそも Preview ではレイアウトを確認したいだけなので、ViewModel のインスタンス化時の処理等々は不要であり、ここをモック化することで対処しました。 以下実装例になります。
@Preview(showBackground = true) @Composable fun PreviewMiniPlayerView() { val mockkMiniPlayerViewModel = mockk<MiniPlayerViewModel>() every { mockkMiniPlayerViewModel.viewData } returns MutableStateFlow( MiniPlayerViewData( isHidden = false, ・・・(略) ) ) MiniPlayerView( viewModel = mockkMiniPlayerViewModel, onTap = {}, onPlayTap = {}, onPauseTap = {}, onRewindTap = {}, onDragStart = {}, onDragEnd = {}, onDrag = {} ) }
mockk でテスト用のモックを作成し、MiniPlayerView の表示で使用する MiniPlayerViewData だけ任意の値を返すように定義しています。 これにより、init 等の処理をスキップしつつ、プレビュー表示に必要な値を返すようにし、Render issue を回避しました。
なお、通常 mockk 等のモックライブラリはユニットテスト目的で使用するために testImplementation として依存関係を追加すると思いますが、Preview で使いたい場合はそれだと import できず、implementation として依存を持つ必要があります。
implementation(libs.mockk)
ちなみに今回試した mockk のバージョンは 1.14.2です。バージョンによっては mockk<{対象クラス名}>
でモックする際に、下記のエラーが出力される場合があり、その場合は mockk のバージョンを適宜変えてみてください。(mockk のバージョン 1.12.4 で発生しました。)
java. lang. ClassNotFoundException: io. mockk. proxy. jvm. dispatcher. JvmMockKWeakMap at java. lang. ClassLoader. loadClass at java. lang. ClassLoader. loadClass at io. mockk. proxy. jvm. advice. jvm. WeakMockHandlersMap.<init>
おまけ
今回の対応ですが、他のモックライブラリとして有名な Mockito でも同様のことができます。 以下実装例です。
@Preview(showBackground = true) @Composable fun PreviewMiniPlayerView() { val mockMiniPlayerViewModel = Mockito.mock(MiniPlayerViewModel::class.java) Mockito.`when`(mockMiniPlayerViewModel.viewData).thenReturn( MutableStateFlow( MiniPlayerViewData( isHidden = false, ・・・(略) ) ) ) MiniPlayerView( viewModel = mockMiniPlayerViewModel, onTap = {}, onPlayTap = {}, onPauseTap = {}, onRewindTap = {}, onDragStart = {}, onDragEnd = {}, onDrag = {} ) }
注意点として、Mockito で対象クラスをモックする場合、モック対象クラスやプロパティに open 修飾子を付与して継承可能とする必要があります。open を付けない状態だと、下記のような MockitoException が出力されます。
org. mockito. exceptions. base. MockitoException: Cannot mock/ spy class ・・・ Mockito cannot mock/ spy because : - final class
終わりに
mockk を使った Preview 表示を試したことで、JUnit 以外に活用の場面があることを知りました。 クラスやメソッド引数で制御したり、BuildConfig を使って制御する等の方法もありますが、プロダクションコードにそのような制御分岐が散乱するのを避けたく、今回の方法を思いついたので試してみました。 この記事が何か参考になるところがあれば嬉しいです。 ここまでお読みいただきありがとうございました!