個人開発で長く保守している壁紙アプリの設定画面に、AdMob バナーを一枚だけ置いています。ある晩、その画面のスクリーンショットテストを整えようとして、手が止まりました。同じコード、同じ端末設定、同じロケール。それなのに、実行するたびにテストが赤くなるのです。差分ビューアを開くと、変わっていたのは広告クリエイティブの中身だけでした。
最初は「たまたま」と思い、無視することにしました。ところが、そのうち本当に見るべきレイアウトの崩れが混ざり込んでも、私はもう差分ビューアを開かなくなっていました。狼が来たと叫び続けたテストは、狼が来た日に信じてもらえません。広告つきの画面を守るには、まず広告そのものを検知の外へ追い出す必要があります。
なぜ広告つきの画面は毎回赤くなるのか
スクリーンショットテストは、描画結果をピクセル単位で基準画像と比べます。ここに、開発者が制御できない要素が一つでも混ざると、テストは本質的に非決定的になります。AdMob バナーは、その最たるものです。
広告の中身はサーバーが毎回決めます。読み込みのタイミングによっては、キャプチャ時にまだ空だったり、途中まで描かれていたりもします。表示される高さも、クリエイティブによって数ピクセル揺れることがあります。次の表は、広告つき画面に潜む非決定性の要因を整理したものです。
| 要因 | 症状 | 封じ方の方針 |
|---|---|---|
| クリエイティブの内容 | 毎回まったく違う画像・文言が入る | テスト時は広告を描かず固定プレースホルダーへ差し替える |
| 読み込みタイミング | 空・半分・完了で結果が変わる | そもそも実広告を読み込ませない |
| 実測高さの揺れ | 数ピクセルのずれで周囲がずれる | スロットの高さを固定し、レイアウトを契約として検証 |
| ネットワーク依存 | CI で読み込み失敗し結果が不安定 | テストをネットワークから切り離す |
要点は一つです。広告を「うまく比較する」のではなく、「比較の対象から外す」。そこを起点に設計します。
広告の描画方針をコードから切り離す
いちばん効いたのは、広告を描くかどうかを画面のコードに埋め込まず、外から差し替えられるようにしたことです。Compose なら CompositionLocal がちょうど良い道具でした。画面は「ここに広告スロットがある」とだけ宣言し、実際に何を描くかは呼び出し側が決めます。
enum class AdRendering { LIVE, PLACEHOLDER }
// 既定は本番描画。テストだけがこれを差し替える
val LocalAdRendering = staticCompositionLocalOf { AdRendering.LIVE }
@Composable
fun AdBannerSlot(modifier: Modifier = Modifier) {
when (LocalAdRendering.current) {
AdRendering.PLACEHOLDER ->
// 実バナーと同じ実測高さの箱を置く。中身は決定的
Box(
modifier
.fillMaxWidth()
.height(50.dp)
.background(Color(0xFFECECEC))
.testTag("ad_slot")
)
AdRendering.LIVE ->
AndroidView(
modifier = modifier
.fillMaxWidth()
.height(50.dp)
.testTag("ad_slot"),
factory = { ctx ->
AdView(ctx).apply {
setAdSize(AdSize.BANNER)
// Google 公式のテスト用バナー ID。実 ID は本番設定から注入する
adUnitId = "ca-app-pub-3940256099942544/6300978111"
loadAd(AdRequest.Builder().build())
}
}
)
}
}画面側は、広告があることを知りつつも、その中身には関与しません。
@Composable
fun SettingsScreen(state: SettingsState) {
Column {
SettingsHeader(title = "設定")
SettingsList(items = state.items)
AdBannerSlot() // 何を描くかは LocalAdRendering が決める
SettingsFooter(version = state.version)
}
}この一手で、テストは「広告を待つ」も「広告を比べる」もしなくなります。描かれるのは常に同じ灰色の箱だけです。広告 SDK をテスト用に差し替えるライブラリを探すより、描画の分岐点を一つ設けるほうが、依存も設定も増えずに済みました。