Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
どうすりゃいいんだ? こうすりゃいいんだ モバイルアプリの設計とテストコード
Search
akito hagio (おはぎ)
October 17, 2025
Programming
67
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
どうすりゃいいんだ? こうすりゃいいんだ モバイルアプリの設計とテストコード
akito hagio (おはぎ)
October 17, 2025
More Decks by akito hagio (おはぎ)
See All by akito hagio (おはぎ)
オライリーサブスクで名著の原著に挑戦 読書シェア会 vol.4
ohagi1014
0
160
Other Decks in Programming
See All in Programming
Web Framework Benchmark Battle 2026, Any%
a_matsuda
2
2.5k
ChromeでローカルLLM
ryu1013
0
140
ジョジョの世界をXRで再現する
mofurune
0
170
[2026-09-26]空論ジェネリックプロセス~テスト資産とAIで紡ぐ、再現可能なパフォーマンスチューニングの話~
tosite
0
270
Compiling a real-world CLI tool to native with Static Hermes
tmikov2023
0
120
【加筆修正版】Laravel のアプリケーションをどこにデプロイするか #phpcon_ehime
akase244
0
150
R8 の設定見直しでアプリサイズ削減
andpad
0
220
Railsのためのパスキー実践入門 〜マルチテナントSaaSでの意思決定〜
utakaha
3
670
最近”作らせてる” ローカルAIアプリの話
akit37
0
480
npm との違いを手がかりにGo の依存管理を理解したい
optim
0
780
Workers Cache を知る
syumai
0
370
機械に任せるテスト、人が見るテスト ⽣成AIで引き直した、運⽤保守フェーズの線引き
dske104
0
190
Featured
See All Featured
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.5k
Future Trends and Review - Lecture 12 - Web Technologies (1019888BNR)
signer
PRO
0
3.8k
BBQ
matthewcrist
90
10k
Scaling GitHub
holman
464
140k
Making the Leap to Tech Lead
cromwellryan
135
10k
Dominate Local Search Results - an insider guide to GBP, reviews, and Local SEO
greggifford
PRO
0
370
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1.1k
Faster Mobile Websites
deanohume
310
32k
Fantastic passwords and where to find them - at NoRuKo
philnash
53
4k
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
570
The untapped power of vector embeddings
frankvandijk
2
1.9k
Un-Boring Meetings
codingconduct
0
460
Transcript
どうすりゃいいんだ?こ うすりゃいいんだ モバイルアプリ 設計とテストコード お ぎ@ oicy
今日 主題
今日 主題 AMIと救い
自己紹介 お ぎ(萩尾亮斗) 株式会社Voicy / テックリード モバイルアプリ開発(2019~) 趣味:読書、スポーツ観戦 Voicyで配信中!
None
目次 • 今⽇話すこと、話さないこと • モバイルアプリのつらみ • つらみの本質 • テストしやすい設計 •
実践例 • テスト戦略 • まとめ
今日話すこと、話さないこと 今⽇話すこと • 主にテストコードの話 • テストコードを書きやすい設計 • モバイル開発での実践例 話さないこと (でも重要)
• テストコードに関係しない話 • 端末の多様性 • E2Eテストツールなどの話 発表で 触れられなかった話題もぜひOSTで話しましょう!
つらみ①:アプリ固有 状態遷移:ライフサイクル 状態遷移 複雑さ • バックグラウンド ⇄ フォアグラウンド • メモリ不足で
強制終了 ライフサイクルメソッド • Android: onCreate, onResume, onDestory... • iOS: viewDidLoad, viewWillAppear… どこで 何をすべきか? いつ 保存‧復元する? どうテストする?
つらみ②:多様な非同期処理 多様な非同期処理 API通信、内部DBアクセス、ファイルアクセス、 位置情報 取得、メディア再生、 Bluetooth 連鎖する非同期処理(Voicy 例) 広告タグ取得 →
広告素材取得 → 広告再生 → 音声再生 +エラーハンドリング、タイムアウト、キャンセル タイミング依存 バグ 再現困難 コールバック地獄になりがち
つらみ③:UI と画面遷移 • 多様な遷移パターン ◦ iOS: Push/モーダル遷移、Android: Fragment遷移、バックスタック ◦ 画面間で
データ 受け渡しや結果 返却 • 画面サイズ対応 ◦ Adaptive Layout(スマホ/タブレット/フォルダブル) • アニメーション ◦ スライドイン、フェード、拡大縮小 ◦ タイミング依存 挙動 複雑になることもあるUI固有 ロジック ビジネスロジックが混在すると複雑さが増す
境界を引け 、テスト 劇的にやりやすくなる! つらみ 本質:境界 喪失 • 外部依存(SDK)と ◦ Activity,
ViewController, MediaPlayer, Room, Core Data... • 何が起きているか ◦ UIコンポーネントにビジネスロジックが散在 ◦ ViewModelが直接SDKを呼び出し • 結果 ◦ SDKを動かさないとテストできないため実機やエミュレータが必須 ◦ 遅く不安定なテスト
Inbound / Core / Outbound • Inbound(入り口) ◦ アプリ内:ユーザー操作、APIレスポンス ◦
アプリ外:通知、ディープリンク ◦ ライブラリ:Activity, Fragment, ViewController • Core(心臓部) ◦ 純粋なビジネスロジック ◦ SDK/DB/UIに依存しない • Outbound(出口) ◦ アプリ内:UI 表現、DB ◦ 端末内:位置情報、広告SDK ◦ 端末外:APIリクエスト Inbound Core Outbound DataFlow DataFlow Dependency Dependency
• 依存 方向 ◦ Inbound → Core ← Outbound ◦
Core 何にも依存しない • メリット ◦ Core テスト 簡単、高速、安定 ◦ PureなKotlin/Swiftで記述される ◦ ロジックがプラットフォームから独立する 依存 方向が重要 Inbound Core Outbound DataFlow DataFlow Dependency Dependency
• 問題 ◦ ExoPlayer/AVAudioPlayerを直接使用 ◦ プラットフォーム固有 詳細に依存 ◦ テストが困難 •
解決策:インターフェース定義 インターフェースで抽象化(1) Interface Abstraction Layer Outbound Core DataFlow Dependency interface AudioPlayer { suspend fun play(audioId: String) suspend fun pause() }
• メリット ◦ ビジネスロジック Interfaceだけに依存 ◦ MediaPlayer 詳細 隠蔽 ◦
テスト時 テストダブル(ex. Fake)を使用 インターフェースで抽象化(2) Interface Abstraction Layer Outbound Core DataFlow Dependency class ExoPlayerAdapter( private val context: Context ) : AudioPlayer { private var exoPlayer: ExoPlayer? = null override suspend fun play(audioId: String) { mediaPlayer = ExoPlayer.create(context, ...) mediaPlayer?.start() } }
Inject • 注意:単体テストにおいてMockを濫用しない ◦ 一貫した振る舞いを実現するため 最小限 ◦ 単に「メソッドが呼 れたか」だけ 不十分
依存性注入 & Mock or Fake Interface Abstraction Layer Outbound Core DataFlow Implement class MusicViewModel( private val audioPlayer: AudioPlayer // 外から注入 ) : ViewModel() { fun playMusic() { ... } }
アプリ固有の型への変換(1) data class ContentUiState( val userName: String, val shouldShowPaywall: Boolean
) • 問題 ◦ APIレスポンスをUI層まで持ち込む ◦ 外部システム 都合がアプリ全体に漏れる • 解決策:アプリ固有 型を定義 ◦ UserResponse(APIレスポンス) ◦ ContentUiState(UI状態) Inbound Core Outbound DataFlow DataFlow Dependency Dependency APIレスポンス UI状態
アプリ固有の型への変換(2) fun UserResponse.toContentUiState(): ContentUiState { return ContentUiState( userName = this.name,
shouldShowPaywall = !this.isPremiumMember ) } @Test fun `プレミアム会員 ペイウォールをスキップ `() { val response = UserResponse( name = "太郎", isPremiumMember = true, ) val state = response.toContentUiState() assertFalse(state.shouldShowPaywall) } Pureな関数として表現された、テスト可能性が高いビジネスロジック
Before:すべてが混在 class PlayerActivity : AppCompatActivity() { override fun onCreate(...) {
lifecycleScope.launch { val adTag = api.getAdTag() val adUrl = api.getAdCreative(adTag.id) val adPlayer = ExoPlayer.create(...).apply { /* 音声を再生する */ } adPlayer.setOnCompletionListener { player = ExoPlayer().apply { /* 広告を再生する */ } } } } } 実装とロジック が混在 SDKを直接使用している Activityなしに テスト不可
After:全体構成 Inbound PlayerViewModel Core PlaybackUseCase Outbound AudioPlayer AdRepository DataFlow DataFlow
Dependency Dependency 各層 役割を明確に分離 • Inbound ◦ Coreロジックを注入される • Core ◦ 純粋なビジネスロジック表現 • Outbound ◦ 抽象化された外部依存呼び出し
class PlayerViewModel(...) { fun play(audioUrl: String) { playbackUseCase .playWithAd(audioUrl) }
} After:各層の実装 interface AudioPlayer { suspend fun play(url: String) } class ExoPlayerAdapter : AudioPlayer { ... } class PlaybackUseCase(...) { suspend fun playWithAd(audioUrl: String) { audioPlayer.play(adUrl) audioPlayer.play(audioUrl) } } Inbound Core Outbound 境界を引いた設計
After:テスト 高速・安定・SDK不要 @Test fun `広告再生後に音声を再生する `() = runTest { val
fakePlayer = FakeAudioPlayer() val fakeAdRepository = FakeAdRepository() val useCase = PlaybackUseCase(fakePlayer, fakeAdRepository) useCase.playWithAd("https://example.com/audio.mp3") assertEquals(2, fakePlayer.playedUrls.size) } Pureに記述された ロジック 高速に実行されるテスト
Pure ロジック層のユニットテスト • Pure ロジック層と ◦ Core層 外部依存を持たないロジック ◦ SDK/DB/UIに依存しない
• 3つ メリット ◦ 高速・安定・書きやすい • 効果 ◦ ビジネスロジック 正しさを直接検証 ◦ 開発サイクルを高速化 ◦ リファクタリングも安心 テスト 「つらい」→「楽しい」 に!
ここから始めよう:Adapter & 統合テスト 段階的な改善とガードレール • Adapter パターン ◦ 責務:SDK 隠蔽
み ◦ ビジネスロジック 書かない ◦ レガシーコード改善にも有効 ◦ 薄く保つ • 統合テスト ◦ 単体テストが優先 ◦ レガシーコード ガードレールとして有効 ◦ 振る舞いを固定→リファクタ→単体テストに置換 今、私も苦戦してます。OSTで語り合いましょう!
まとめ 境界を引け 、テスト 楽になる • 根本原因 ◦ 外部依存とビジネスロジック 境界が曖昧 •
テストしやすい設計 ◦ Inbound/Core/Outbound で依存 方向を明確に ◦ インターフェースでSDKを隠蔽 ◦ アプリ固有 型で Pure なロジックを実現 ◦ Pure 層 ユニットテストに注力 ◦ Adapter で段階的改善 ◦ どこをテストすべきか、どうやってテストすべきかが洗練される 一歩ずつ改善すれ 、「つらい」→「楽しい」
Thank you! 一緒にテストしやすいアプリを作っていきましょう! ぜひOSTで語り合いましょう! Voicy採用ページ 募集中 エンジニアポジション We are hiring!