$30 off During Our Annual Pro Sale. View Details »

iOSアプリ開発における効果的なUnitTestの戦略とその実装

GO Inc. dev
September 28, 2023

 iOSアプリ開発における効果的なUnitTestの戦略とその実装

後夜祭 iOSDC Japan 2023で発表した資料です。

GO Inc. dev

September 28, 2023
Tweet

More Decks by GO Inc. dev

Other Decks in Programming

Transcript

  1. © GO Inc.
    iOSアプリ開発における効果的
    なUnitTestの戦略とその実装
    2023.09.26
    開発本部 ソフトウェア開発統括部 ユーザーシステム開発部 ユーザーシステム1グループ / 髙橋秀宗
    GO株式会社

    View Slide

  2. © GO Inc. 2
    自己紹介
    GO株式会社
    ユーザーシステム1グループ / 髙橋秀宗
    2022年12月に入社。タクシーアプリ『GO』のiOSアプリ
    開発を担当
    趣味はエレキギター演奏
    お気に入りのアンプはMarshall JCM800
    @h1d3mun3

    View Slide

  3. Index
    © GO Inc.
    1. UnitTestとは
    2. 単一責任原則
    3. 実践UnitTest
    4. まとめ
    5. 参考資料
    3

    View Slide

  4. © GO Inc.
    UnitTestとは
    01

    View Slide

  5. © GO Inc.
    V字モデル
    5
    実装
    基本設計 結合テスト
    詳細設計
    コンポーネントテスト
    (単体テスト
    ・UnitTest)
    要件定義
    システム/受け入れ
    テスト
    対応するテストスコープ
    https://shiftasia.com/ja/column/v%E5%AD%97%E3%83%A2%E3%83%87%E3%83%AB%E3%81%A8%E3%81%AF/

    View Slide

  6. © GO Inc.
    V字モデル
    6
    実装
    基本設計 結合テスト
    詳細設計
    コンポーネントテスト
    (単体テスト
    ・UnitTest)
    要件定義
    システム/受け入れ
    テスト
    対応するテストスコープ
    https://shiftasia.com/ja/column/v%E5%AD%97%E3%83%A2%E3%83%87%E3%83%AB%E3%81%A8%E3%81%AF/

    View Slide

  7. © GO Inc.
    ● 詳細設計で実装したものをテストする
    ○ 作ったクラスや、変更した処理など
    ● 要件定義・基本設計レベルの観点はテストしない
    UnitTestのスコープ
    7

    View Slide

  8. © GO Inc.
    ● ホワイトボックステスト
    ○ システムの内部も考慮したテスト
    ○ if式やswitch式、guard式などが動くかどうかも考慮する
    ● ブラックボックステスト
    ○ システムの外部をテストする
    ○ 入力に対して期待する出力かどうかを考慮する
    ホワイトボックステスト・ブラックボックステスト
    8

    View Slide

  9. © GO Inc.
    ● 🙆 メリット 🙆
    ○ 高速に、何度も、正確に実施できる
    ○ 実施コストが低い
    ● 🙅 デメリット🙅
    ○ 状況に応じて操作を分けるのが難しい
    ○ 人間的な操作など、非規則的なテストが難しい
    UnitTestの特徴
    9

    View Slide

  10. © GO Inc.
    ● テスト対象
    ○ 詳細設計に対応するので、実装したクラスや処理などをチェック
    ● コスト
    ○ 実行コストが低いので、CI上でDailyで回すなどして、問題の早期発見に活
    用する
    ● テスト方式
    ○ 問題の早期発見を主眼にするので、クラスや処理の入力と出力を見るブラッ
    クボックステストを行う
    UnitTestの効果的な使い方
    10

    View Slide

  11. © GO Inc.
    単一責任原則
    02

    View Slide

  12. © GO Inc.
    ● モジュールはたったひとつのアクターに対して責務を負うべきである、という原

    ● アクターとは「ユーザーやステークホルダーのグループ」のこと
    ○ もちろんソースコード内部の別ファイルもアクターになりうる 🎉
    → これが守られていると、モジュール(≒ソースコード)を変更する理由は必ず1つに
    なる
    単一責任原則
    12

    View Slide

  13. © GO Inc.
    実践UnitTest
    03

    View Slide

  14. © GO Inc.
    1. 既存のコードをアクターを単位にして分割する
    2. 分割したコードの振る舞いをProtocolに抽象化しDI可能にする
    3. それぞれに対してMockを利用してテストをする
    既存のコードにUnitTestを適用させる
    14

    View Slide

  15. © GO Inc.
    やってみよう!
    ● Notification構造体でアプリ内お知らせを表現
    ● NotificationManagerクラスが、アプリ内お知らせを管理
    し、下記を行っている
    ○ 現在のお知らせ一覧を返却する機能
    ○ メッセージの既読処理
    ○ APIを経由したサーバーへの反映処理
    15

    View Slide

  16. © GO Inc.
    Notification構造体
    struct Notification {
    let title: String
    let isRead: Bool
    }
    16

    View Slide

  17. © GO Inc.
    NotificationManagerクラス
    class NotificationManager {
    func getAllUnReadNotifications() -> [Notification] { 〜〜〜 }
    func markAsRead(notification: Notification, completion: @escaping (() -> Void)) { 〜〜〜 }
    func postApi(notification: Notification, completion: @escaping (() -> Void)) { 〜〜〜 }
    }
    17

    View Slide

  18. © GO Inc.
    修正前クラス図
    18

    View Slide

  19. © GO Inc.
    1. 既存のコードをアクターを単位にして分割する
    2. 分割したコードの振る舞いをProtocolに抽象化しDI可能にする
    3. それぞれに対してMockを利用してテストをする
    再掲: 既存のコードにUnitTestを適用させる
    19

    View Slide

  20. © GO Inc.
    各責務をそれぞれ別々のファイルに分割する
    ● メッセージの既読処理
    ○ → MarkAsReadUseCase
    ● 現在のお知らせ一覧を返却する機能
    ○ → NotificationRepository
    ● APIを経由したサーバーへの反映処理
    ○ → NotificationServerDataStore
    既存のコードをアクターを単位にして分割する
    20

    View Slide

  21. © GO Inc.
    修正後クラス図
    21

    View Slide

  22. © GO Inc.
    1. 既存のコードをアクターを単位にして分割する
    2. 分割したコードの振る舞いをProtocolに抽象化しDI可能にする
    3. それぞれに対してMockを利用してテストをする
    再掲: 既存のコードにUnitTestを適用させる
    22

    View Slide

  23. © GO Inc.
    UnitTestのときにMockを作成しやすくするため、責務をProtocolに抽象化する
    分割したコードの振る舞いをProtocolに抽象化しDI可能にする
    23

    View Slide

  24. © GO Inc.
    修正後クラス図
    24

    View Slide

  25. © GO Inc.
    1. 既存のコードをアクターを単位にして分割する
    2. 分割したコードの振る舞いをProtocolに抽象化しDI可能にする
    3. それぞれに対してMockを利用してテストをする
    再掲: 既存のコードにUnitTestを適用させる
    25

    View Slide

  26. © GO Inc.
    UnitTestを書く 〜 MarkAsReadUseCaseImpl
    26
    protocol MarkAsReadUseCase {
    func execute(notification: Notification, completion: @escaping (() -> Void))
    }
    struct MarkAsReadUseCaseImpl: MarkAsReadUseCase {
    let notificationRepository: NotificationRepository
    func execute(notification: Notification, completion: @escaping (() -> Void)) {
    notificationRepository.store(notification: readNotification, completion: completion)
    }
    }

    View Slide

  27. © GO Inc.
    UnitTestを書く 〜 MarkAsReadUseCaseImpl
    27
    MarkAsReadUseCaseImplの役割は1つ
    1. RepositoryのStore関数を呼び出す
    → 上記が正しく動作することをUnitTestで確認する

    View Slide

  28. © GO Inc.
    UnitTestを書く 〜 MarkAsReadUseCaseImpl
    28
    1. NotificationRepositoryのMockを作る
    2. MockをDIしてMarkAsReadUseCaseImplを初期化する
    3. テスト用のNotificationを利用してmarkAsRead関数を呼び出す。
    4. Repositoryの関数呼び出し回数と、取り扱っているデータが正しいことを確認
    する

    View Slide

  29. © GO Inc.
    NotificationRepositoryのMockを作る
    29
    struct FakeNotificationRepository: NotificationRepository {
    func getAllNotification() -> [Notification] {
    return []
    }
    var storeCallCount = 0
    var storeArguments: Notification!
    func store(notification: Notification, completion: @escaping (() -> Void)) {
    storeCallCount += 1
    storeArguments = notification
    completion()
    }
    }

    View Slide

  30. © GO Inc.
    let fakeRepository = FakeNotificationRepository()
    execute関数のテストを書く
    30

    View Slide

  31. © GO Inc.
    let fakeRepository = FakeNotificationRepository()
    let subject = MarkAsReadUseCaseImpl(notificationRepository: fakeRepository)
    execute関数のテストを書く
    31

    View Slide

  32. © GO Inc.
    let fakeRepository = FakeNotificationRepository()
    let subject = MarkAsReadUseCaseImpl(notificationRepository: fakeRepository)
    let fakeNotification = Notification(title: "テストお知らせ", isRead: false)
    subject.execute(notification: fakeNotification, completion: { in })
    execute関数のテストを書く
    32

    View Slide

  33. © GO Inc.
    let fakeRepository = FakeNotificationRepository()
    let subject = MarkAsReadUseCaseImpl(notificationRepository: fakeRepository)
    let fakeNotification = Notification(title: "テストお知らせ", isRead: false)
    subject.execute(notification: fakeNotification, completion: { in })
    expect(fakeRepository.storeCallCount).to(equal(1))
    expect(fakeRepository.storeArguments).to(equal(fakeNotification))
    execute関数のテストを書く
    33

    View Slide

  34. © GO Inc.
    UnitTestを書いたことによる副次的なメリット
    ● アクターをベースとした責務で切り分けたので、既存コードに対して修正を行っ
    た際の意図せぬ副作用のリスクを低減できた
    34

    View Slide

  35. © GO Inc.
    まとめ
    04

    View Slide

  36. © GO Inc.
    まとめ
    36
    ● UnitTestは詳細設計に対応するテストで、クラスや処理をテストする
    ● UnitTestをCI上でDailyで回すなどして問題の早期発見に活かすと良い
    ● 既存コードに適用する場合は、アクターという観点で既存コードを分解してから
    UnitTestを組む

    View Slide

  37. © GO Inc.
    参考資料
    05

    View Slide

  38. © GO Inc.
    まとめ
    38
    ● V字モデルとは
    ○ https://shiftasia.com/ja/column/v%E5%AD%97%E3%83%A2%E3%83
    %87%E3%83%AB%E3%81%A8%E3%81%AF/
    ● ホワイトボックステストとは
    ○ https://service.shiftinc.jp/column/4801/
    ● solid+cqs+dry
    ○ https://speakerdeck.com/kgmyshin/solid-plus-cqs-plus-dry

    View Slide

  39. 文章・画像等の内容の無断転載及び複製等の行為はご遠慮ください。
    © GO Inc.

    View Slide