カルボナーラ街道

計測と観察

SwiftUIでGraphQL Fragment Colocation利用時におけるプレゼンテーションロジックのテストを考える

テストを考える季節。

開発環境

> xcodebuild -version
Xcode 14.0
Build version 14A309

apollo-ios: 0.53.0

モチベーション

Fragment Colocation利用時におけるプレゼンテーションロジック(ここでは表示用の小数点丸め込みなどのロジックを指す)はどこでテストするの?問題を整理したい。

Fragment Colocation

www.apollographql.com

コンポーネント、View単位で必要なデータを一つのFragmentにまとめてクエリする。

プレゼンテーションロジックのテストを考える

fragment DailyWorkoutItem on Workout {
    distance
    type
}
import Apollo
import SwiftUI

struct DailyWorkoutView: View {
    let item: DailyWorkoutItem
    
    var body: some View {
        VStack {
            // (本題とそれるが)なぜswitchを使わない?
            // -> Apollo-iOS はEnumに対して自動で `__unknown` caseを追加する。
            // switchを使う場合は `__unknown` に対して空表示用のViewを差し込む必要が出て考えることが増える
            // ならいっそのことif caseで `__unknown` 以外の場合のみViewを差し込めば良いのでは、という考え
            if case .running = item.type {
                Text("ランニング")
            } else if case .cycling = item.type {
                Text("サイクリング")
            }
            Text(String(format: "%.2f", item.distance))
        }
    }
}

Flagment Colocationの簡単な例。気になっているのは以下の行。

Text(String(format: "%.2f", item.distance))

シンプルにFragment Colocationを利用するとプレゼンテーションロジックをViewが持つようになる。こういうのはテストしておきたい。上記の実装ベースにテストを行う場合は以下の手法が考えられそう。

  1. XCUIApplication経由でViewの値を取得してテスト*1
  2. nalexn/ViewInspector*2経由でViewの値を取得してテスト

1の場合は .accessibilityIdentifier などを使ってViewの要素をアプリ全体で一意に識別できる状態にする必要があったり、launch() を実行してコード上でアプリを実行させる必要がある。 2は概念が1と同じ。テスト対象のViewを見つけやすくする工夫を施す必要がある。

一方で以下のようにテストしても良いのではないか。

import Apollo
import SwiftUI

struct DailyWorkoutView: View {
    let item: DailyWorkoutItem
    
    var body: some View {
        VStack {
            if case .running = item.type {
                Text("ランニング")
            } else if case .cycling = item.type {
                Text("サイクリング")
            }
            // 変更
            Text(formatDistanceString(item.distance))
        }
    }
    
    // 追加
    func formatDistanceString(_ from: Double) -> String {
        return String(format: "%.2f", from)
    }
}
// テスト
func testFormatDistanceString() {
    let item = DailyWorkoutItem(
        distance: 123.456789,
        type: .cycling
    )
    let dailyWorkoutView = DailyWorkoutView(item: item)
    let actual = dailyWorkoutView.formatDistanceString(item.distance)
    let expection = "123.46"
    XCTAssertEqual(actual, expection)
}

上記は対象のメソッドが実際に利用されていることが担保されていないのでそこがネック。個人の思想だがプレゼンテーションロジックのテストのためにアプリを起動したり、任意にViewを特定できる仕組みを追加するのは手間と感じる。シンプルにView側で明確なin-outを持つメソッドを用意してそれをテストすれば良いのでは、という考え。また煮詰まったらブログに書く。今の所上記の全てに納得がいってなくて良い着地点を見つけたい。

参考