Avatar
Avatar
koher
僕は次の二つの方法のいずれかを用いています(スナップショットテストはしていませんが、 Xcode Preview のためにやっています)。 ① 一つは、 @StateObject を保持する View とレイアウトする View を分離し、前者が後者を利用する形にすることです。 // state 保持用の View とレイアウト用の View を分離する例 struct FooStateView: View { @StateObject private var state: FooViewState<FooService> ... var body: some View { FooView(bar: state.bar, baz: $state.baz) } } struct FooView: View { let bar: Int @Binding var baz: String ... } こうしておけば state とは独立に FooView の任意の状態を簡単に実現できるので、スナップショットテストやプレビューのためのモックが必要なくなります。↑は SwiftUI の例ですが、UIKit の場合でも、外から渡された状態を再現するだけの VC と、 state を保持して状態を伝えるだけの VC とに分離することで、同様のことが実現可能です。 ② もう一つは、公開する state のプロパティをすべて @Publsihed public var にしてしまうことです。そうすれば、スナップショットテストやプレビューの際に、任意の状態の state を簡単に作れます。 UserService 等にはダミーのテスト用実装を渡して何の仕事もさせないようにします。なお、 computed property の場合は setter を作れないので、↓のように republish する必要が生じます。 // computed property の代わりに republish が必要になる例 // Before @MainActor public final class FooViewState<FooService: FooServiceProtocol>: ObserbalbleObject { @Published public var baz: String = "" public var isQux: Bool { baz.isEmpty } ... } // After @MainActor public final class FooViewState<FooService: FooServiceProtocol>: ObserbalbleObject { @Published public var baz: String = "" @Published public var isQux: Bool = false ... init(...) { ... $baz.map(\.isEmpty).assign(to: &$isQux) // republish } ... } ①②とも一長一短で、①は実装が冗長になりがちです。②は外から触っていいものといけないもの(本来 private(set) であるべきもの)の区別が付きづらいです。テスト以外のケースで、本来メソッドを通して状態を変更しなければならないプロパティを setter で変更してしまうと、 state の状態を壊してしまう可能性があります。 @testable import を前提に、 internal(set) にするのも良いかもしれません。
なるほどありがとうございます!!特に internal(set) で対処するのが自分のユースケースにピッタリ合っていて非常に勉強になります 🙇‍♂️
🙂 1