※ChatGPTを使用して記事を作成しています。
Jetpack Composeを導入したチームで、よくこんな会話を聞きます。
「ComposeってUIテスト難しくない?」
「テスト書こうとするとViewModelが必要になるんだよね…」
しかし実際は逆です。
ComposeはUIテストが非常に書きやすいUIフレームワークです。
ただし条件があります。
UIの設計が正しくできている場合だけです。
今回は
ComposeでテストしやすいUIを書くための設計
について整理します。
なぜUIテストが難しくなるのか
まずは、テストしにくいComposableを見てみましょう。
@Composable
fun UserProfile(viewModel: UserViewModel = viewModel()) {
val user by viewModel.user.collectAsState()
Column {
Text(user.name)
Button(
onClick = {
viewModel.followUser()
}
) {
Text("Follow")
}
}
}
このコードには、UIテストを難しくする要因が3つあります。
問題① ViewModel依存
viewModel: UserViewModel
テスト時に
- Fake ViewModel
- Mock ViewModel
などを用意する必要があります。
問題② State取得がUI内部
viewModel.user.collectAsState()
UIが 状態取得の責務 を持っています。
問題③ ロジック依存
viewModel.followUser()
UIが直接ロジックを呼び出しています。
つまりこのUIは
UI + State + Logic
が混ざっています。
この構造ではテストが難しくなります。
テストしやすいUIの基本構造
ComposeでテストしやすいUIは、次の形になります。
Composable(
state,
event
)
つまり
- 状態は引数
- イベントはコールバック
です。
改善したUI
先ほどのコードを改善します。
@Composable
fun UserProfile(
userName: String,
onFollowClick: () -> Unit
) { Column {
Text(userName)
Button(
onClick = onFollowClick
) {
Text("Follow")
}
}
}
これで何が変わるでしょうか。
このUIは
- ViewModel不要
- Repository不要
- API不要
になります。
つまり
UI単体でテスト可能 になります。
Compose UIテストの書き方
ComposeのUIテストは非常にシンプルです。
@get:Rule
val composeTestRule = createComposeRule()
テスト対象をセットします。
composeTestRule.setContent {
UserProfile(
userName = "Taro",
onFollowClick = {}
)
}
そしてUIを検証します。
composeTestRule
.onNodeWithText("Taro")
.assertExists()
これだけです。
ボタンイベントのテスト
イベントのテストも簡単です。
@Test
fun followButtonClick() {
var clicked = false
composeTestRule.setContent {
UserProfile(
userName = "Taro",
onFollowClick = {
clicked = true
}
)
}
composeTestRule
.onNodeWithText("Follow")
.performClick()
assert(clicked)
}
ポイントはシンプルです。
UIはイベントを通知するだけ
なのでテストもしやすくなります。
UI Stateを使うとさらにテストしやすい
実務ではStateをまとめて渡すことが多いです。
data class UserProfileUiState(
val name: String,
val isFollowing: Boolean
)
ComposableはStateを受け取ります。
@Composable
fun UserProfile(
state: UserProfileUiState,
onFollowClick: () -> Unit
) {
Column {
Text(state.name)
Button(
onClick = onFollowClick
) {
Text(
if (state.isFollowing)
"Following"
else
"Follow"
)
}
}
}
この設計のメリットは大きいです。
テストでは
任意の状態を自由に作れる
からです。
状態ごとのUIテスト
例えば次のようなテストが書けます。
フォロー前
UserProfile(
state = UserProfileUiState(
name = "Taro",
isFollowing = false
),
onFollowClick = {}
)
期待UI
Follow
フォロー済み
UserProfile(
state = UserProfileUiState(
name = "Taro",
isFollowing = true
),
onFollowClick = {}
)
期待UI
Following
このように
UI状態ごとのテストが簡単に書ける
ようになります。
testTagを使う
実務では testTag を使うことも多いです。
Button(
modifier = Modifier.testTag("followButton"),
onClick = onFollowClick
)
テスト側
composeTestRule
.onNodeWithTag("followButton")
.performClick()
テキストに依存しないテストが書けます。
Composeテストを壊すアンチパターン
実務でよく見る失敗を紹介します。
アンチパターン① ViewModel取得 inside UI
viewModel()
UIテストが困難になります。
アンチパターン② collectAsState inside UI
viewModel.flow.collectAsState()
状態制御ができません。
アンチパターン③ MutableStateを渡す
MutableState<String>
UIが状態を書き換えてしまいます。
Composeテストが強い理由
実はComposeは
UIテストに非常に強い設計
になっています。
なぜなら、
UI = 関数
だからです。
つまり
入力 → UI
の関数テストになります。
従来のView UIとの違い
従来のAndroid UIでは、
Activity
Fragment
View
Adapter
などが絡みます。
UIテストを書くには
- Activity起動
- Fragment表示
- View取得
などが必要でした。
しかしComposeでは
Composable単体
でテストできます。
これは非常に大きな違いです。
まとめ
ComposeでテストしやすいUIを書くための原則はシンプルです。
① UIは状態を受け取る
Composable(state)
② UIはイベントを通知する
onClick
onEvent
③ ViewModelをUIに持ち込まない
Screen → ViewModel
UI → 描画
この構造を守ると、
- Previewしやすい
- 再利用しやすい
- テストしやすい
という Composeの理想設計 になります。
Composeは単なるUIツールではありません。
設計の質がそのままコード品質に直結するフレームワーク
です。
そして、
テストしやすいUIを書くことは
設計が正しいことの証明でもあります。

