Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ComposeのMutableStateってどうやってLocal Unit Testすれば良いの??

ComposeのMutableStateってどうやってLocal Unit Testすれば良いの??

Tomoya Miwa

February 21, 2022
Tweet

More Decks by Tomoya Miwa

Other Decks in Programming

Transcript

  1. UiState UI表示に必要なデータは、1つのUiState classとしてViewModelが公開するのがお勧 め。 data class NewsUiState( val isSignedIn: Boolean

    = false, val isPremium: Boolean = false, val newsItems: List<NewsItemUiState> = listOf(), val userMessages: List<Message> = listOf() ) data class NewsItemUiState( val title: String, val body: String, val bookmarked: Boolean = false, ... ) https://developer.android.com/jetpack/guide/ui-layer#define-ui-state 7
  2. 簡単なケース data class TweetUiState( val isFavorited: Boolean = false )

    class TweetViewModel(): ViewModel() { var uiState by mutableStateOf(TweetUiState()) private set fun toggleFavorite() { uiState = uiState.copy(isFavorited = uiState.isFavorited.not()) } } 20
  3. 単なる変数の変化としてテストすればOK @Test fun TestToggleFavorite() { val viewModel = TweetViewModel() val

    prev = viewModel.uiState.isFavorited viewModel.toggleFavorite() assert(viewModel.uiState.isFavorited == prev.not()) } 21
  4. 難しいケース data class TimelineUiState( val isLoading: Boolean = false, val

    tweets: List<String> = emptyList() ) class TimelineViewModel(): ViewModel() { var uiState by mutableStateOf(TimelineUiState()) private set fun refreshTimeline() { viewModelScope.launch { uiState = uiState.copy(isLoading = true) delay(10) // APIを呼ぶ代わり uiState = uiState.copy( isLoading = false, tweets = listOf("Android 13 Tiramisu", "Android 12L") ) } } } 24
  5. テストが難しいポイントはどこか? isLoadingのテストが難しい data class TimelineUiState( val isLoading: Boolean = false,

    val tweets: List<String> = emptyList() ) class TimelineViewModel(): ViewModel() { var uiState by mutableStateOf(TimelineUiState()) private set fun refreshTimeline() { viewModelScope.launch { uiState = uiState.copy(isLoading = true) // isLoading == true になったあと delay(10) uiState = uiState.copy( isLoading = false, // isLoading == false になる事をテストしたい tweets = listOf("Android 13 Tiramisu", "Android 12L") ) } } } 25
  6. 一部抜粋でご紹介 class ComposeStateTestRule(snapshotIntervalMilliSec: Long = 1L): TestWatcher() { ... private

    val snapshotTaker = flow<Unit> { coroutineScope { while(isActive) { delay(snapshotIntervalMilliSec) Snapshot.takeSnapshot { } } } } override fun starting(description: Description?) { job = snapshotTaker.launchIn(scope) } override fun finished(description: Description?) { job?.cancel() job = null } } 40
  7. 一部抜粋でご紹介 @Test fun testRefreshTimeline() = runTest { val viewModel =

    TimelineViewModel() var output: List<TimelineUiState>? = null val job = launch { output = snapshotFlow { viewModel.uiState } .take(2) .toList() } viewModel.refreshTimeline() job.join() assert(output?.size == 2) assert(output?.get(0)?.isLoading == true) assert(output?.get(1)?.isLoading == false) assert(output?.get(1)?.tweets == listOf("Android 13 Tiramisu", "Android 12L")) } 45
  8. まとめと感想 Compose用のViewModelはStateFlowの公開すら推奨されていない、ということが 個人的にはちょっとビックリ でも、Single Source of TruthやStatelessなComposable関数が好ましい事を考 えるとそれが正しいのかも Flow.collectAsState() は単に内部でMutableStateつくってcollectしてい

    るだけだし、Lifecycleすら考慮されていない ただ、MutableStateはちょっと難しい(でも、良くありそうなケース)ケースの テスト難易度が高いので、公式で補足が欲しいなーというところ 自分が見落としているだけ、という事であればどなたか教えてください! ComposeStateTestRuleで1msecごとにsnapshot取るのはやり過ぎな気もするけ ど、テストだから良いかな、という気持ちです もっと良い方法をご存じの方がいたら、教えてください! 47