Slide 1

Slide 1 text

新規アプリの 単体テスト戦略 2022.10.28(Fri) iOS Test Online @the_uhooi

Slide 2

Slide 2 text

iOS app developer @uhooi @uhooi @the_uhooi

Slide 3

Slide 3 text

入社前の会話 新規アプリ作って!

Slide 4

Slide 4 text

入社前の会話 新規アプリ作って! はい!

Slide 5

Slide 5 text

ウホーイ入社 ウホーイ 入社

Slide 6

Slide 6 text

・UI: フル SwiftUI ・リアクティブ: Combine ・非同期処理: Swift Concurrency (async/await) ・アーキテクチャ: MVVM ・その他: SwiftPM によるマルチモジュール、KMM ※開発中なので変更される可能性が大いにあります。 新規アプリの技術選定

Slide 7

Slide 7 text

最初に考えた単体テストの戦略

Slide 8

Slide 8 text

できる限り にする 単体テストが不要な設計 最初に考えた単体テストの戦略

Slide 9

Slide 9 text

できる限り にする 単体テストが不要な設計 必要な にする 単体テストを書きやすい設計 最初に考えた単体テストの戦略

Slide 10

Slide 10 text

01 単体テストが な 設計にする 不要

Slide 11

Slide 11 text

アーキテクチャ(MVVM) View View
 Model UseCase KMM

Slide 12

Slide 12 text

単体テストを書く層を明文化する View View
 Model UseCase KMM : 単体テストを書かない : 必要に応じて単体テストを書く : 単体テストを書く

Slide 13

Slide 13 text

View 層の単体テストを書かない理由 UI/UX を確認するため、 必ず から 手動でテストを行う

Slide 14

Slide 14 text

どうしたら View 層から単体テストをなくせる? View View
 Model UseCase KMM : 単体テストを書かない : 必要に応じて単体テストを書く : 単体テストを書く

Slide 15

Slide 15 text

ロジックを減らせば単体テストも減らせる View View
 Model UseCase KMM 少なくしたい 多くする : 単体テストを書かない : 必要に応じて単体テストを書く : 単体テストを書く View 層からロジックを なくすのが理想 ロジック

Slide 16

Slide 16 text

ロジックを減らすには? ロジックを減らすには?

Slide 17

Slide 17 text

分岐をなくせばロジックを減らせる ロジックを減らすには? 分岐 をなくす (if 文や switch 文)

Slide 18

Slide 18 text

View に分岐がある(Before) // Before // View 層 if viewModel.uiState.isLoading { Text("ロード中です。") } else { Text("ロードが完了しました。") }

Slide 19

Slide 19 text

View から分岐をなくす(After) // After // View 層 Text(viewModel.uiState.loadingText) // ViewModel 層 var loadingText: String { isLoading ? "ロード中です。" : "ロードが完了しました。" }

Slide 20

Slide 20 text

View から分岐をなくす(After) // After // View 層 Text(viewModel.uiState.loadingText) // ViewModel 層 var loadingText: String { isLoading ? "ロード中です。" : "ロードが完了しました。" } View 層の分岐を ViewModel 層に寄せる

Slide 21

Slide 21 text

それでも View から分岐は完全にはなくせない // View 層 struct MonstersView: View { var body: some View { if viewModel.uiState.monsters.isEmpty { EmptyMonsterListView() } else { MonsterListView(monsters: viewModel.uiState.monsters) } } }

Slide 22

Slide 22 text

それでも View から分岐は完全にはなくせない // View 層 struct MonstersView: View { var body: some View { if viewModel.uiState.monsters.isEmpty { EmptyMonsterListView() } else { MonsterListView(monsters: viewModel.uiState.monsters) } } } View の出し分けなどの 分岐はなくせない

Slide 23

Slide 23 text

それでも View から分岐は完全にはなくせない // View 層 struct MonstersView: View { var body: some View { if viewModel.uiState.monsters.isEmpty { EmptyMonsterListView() } else { MonsterListView(monsters: viewModel.uiState.monsters) } } } View の出し分けなどの 分岐はなくせない monsters が空のときの UI の確認が手間

Slide 24

Slide 24 text

そこでプレビュー そこでプレビュー

Slide 25

Slide 25 text

Xcode 上で SwiftUI の View を確認できる

Slide 26

Slide 26 text

「UI を確認するだけの手動テスト」をなくせる プレビューでは ・端末のサイズ ・ライト / ダークモード ・データの最小 / 最大値 ・エラー時の View  etc... 様々な状態をかんたんに作り出せる

Slide 27

Slide 27 text

02 単体テストを 設計にする 書きやすい

Slide 28

Slide 28 text

ViewModel 層以下の単体テストを書きやすくしたい View View
 Model UseCase KMM : 単体テストを書かない : 必要に応じて単体テストを書く : 単体テストを書く

Slide 29

Slide 29 text

UI フレームワークを View 層のみでインポートする View View
 Model UseCase KMM SwiftUI Combine : 単体テストを書かない : 必要に応じて単体テストを書く : 単体テストを書く View 層のみで UI を完結させる

Slide 30

Slide 30 text

UI フレームワークを View 層のみでインポートする View View
 Model UseCase KMM SwiftUI Combine : 単体テストを書かない : 必要に応じて単体テストを書く : 単体テストを書く ViewModel 層以下がロジックに集中でき、 単体テストが書きやすくなる

Slide 31

Slide 31 text

Task があると単体テストしにくい(Before) // Before // ViewModel 層 struct FooViewModel: ObservableObject { func didTapLoginButton() { Task { await login() } } }

Slide 32

Slide 32 text

Task があると単体テストしにくい(Before) // Before // ViewModel 層 struct FooViewModel: ObservableObject { func didTapLoginButton() { Task { await login() } } } ログイン処理を待たずに テストが終了してしまう

Slide 33

Slide 33 text

ViewModel 層のメソッドを非同期にする(After) // After // ViewModel 層 struct FooViewModel: ObservableObject { func didTapLoginButton() async { await login() } }

Slide 34

Slide 34 text

ViewModel 層のメソッドを非同期にする(After) // After // ViewModel 層 struct FooViewModel: ObservableObject { func didTapLoginButton() async { await login() } } 非同期にすると 並行処理もしやすくなる

Slide 35

Slide 35 text

Task を View 層に書く(After) // After // View 層 struct FooView: View { // ... Task { await viewModel.didTapLoginButton() } // ... }

Slide 36

Slide 36 text

ちなみに…

Slide 37

Slide 37 text

ちなみに… まだ単体テストを書いていません これから書きます!

Slide 38

Slide 38 text

03 まとめ

Slide 39

Slide 39 text

まとめ 設計を工夫することで、 、 できる 不安定な単体テストを減らし 安定した単体テストを書きやすく

Slide 40

Slide 40 text

まとめ View View
 Model UseCase KMM UI ロジック Task 単体テストしにくい 単体テストしやすい

Slide 41

Slide 41 text

まとめ View View
 Model UseCase KMM 単体テストしにくい・しやすいものを 分離し、単体テストする層を明確にする UI ロジック Task 単体テストしにくい 単体テストしやすい

Slide 42

Slide 42 text

まとめ View View
 Model UseCase KMM UI ロジック Task 単体テストを書かない層(View 層)は プレビューや手動テストで品質を担保する 単体テストしにくい 単体テストしやすい