はじめに
iOS 17 から SwiftData が使えるようになってデータの永続化がサクッとできるようになりました。めっちゃ便利です。ただ下記のような実装で fetchPiyo1HogeList
でちゃんと思い通りのデータが取れてるかテスト書きたいなと思ったときに少々めんどうです。どうやってテスト書けばいいんだ?ということについていくつか考えてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Model final class Hoge { var fuga: String var piyo: Int init(fuga: String, piyo: Int) { self.fuga = fuga self.piyo = piyo } } struct ContentView: View { @Environment(\.modelContext) private var modelContext var body: some View { Text(fetchPiyo1HogeList().first!.fuga) } private func fetchPiyo1HogeList() -> [Hoge] { return (try? modelContext.fetch( FetchDescriptor<Hoge>( predicate: #Predicate { $0.piyo == 1 } ) )) ?? [] } } |
案1テストで View を生成する(ボツ案)
ContentView
の fetchPiyo1HogeList
を public
にしてテスト書けるかも?ということで試してみましたが ContetView
のキャスト部分でクラッシュするので無理でした。できたとしても ContentView
にテストデータ作成用メソッドを用意しないといけなくなるのであんまイケてない感じになります。
1 2 3 4 5 6 7 8 9 10 11 |
struct PiyoSampleTests { @MainActor @Test func filterPiyo1HogeList() { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Hoge.self, configurations: config) let view = ContentView().modelContainer(container) as! ContentView let items = view.fetchPiyo1HogeList() #expect(items.count == 1) } } |
案2 ViewModel にわける
正攻法ぽい ViewModel にわける方法です。
参考:How to write unit tests for your SwiftData code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
struct ContentView: View { @Observable final class ViewModel { private let modelContext: ModelContext init(modelContext: ModelContext) { self.modelContext = modelContext } func fetchPiyo1HogeList() -> [Hoge] { return (try? modelContext.fetch( FetchDescriptor<Hoge>( predicate: #Predicate { $0.piyo == 1 } ) )) ?? [] } func addHoge() { modelContext.insert(Hoge(fuga: "fuga", piyo: 1)) } } @State private var viewModel: ViewModel init(modelContext: ModelContext) { let viewModel = ViewModel(modelContext: modelContext) _viewModel = State(initialValue: viewModel) } var body: some View { Text(viewModel.fetchPiyo1HogeList().first!.fuga) } } |
テストはこんな感じです。
1 2 3 4 5 6 7 8 9 |
@MainActor @Test func filterPiyo1HogeList() { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Hoge.self, configurations: config) let viewModel = Content2View.ViewModel(modelContext: container.mainContext) viewModel.addHoge() let items = viewModel.fetchPiyo1HogeList() #expect(items.count == 1) } |
これで無事テストができるようになりました!ただ addHoge
のようなテストデータ作成用メソッドを ViewModel に書かないといけないのでイケてない感じはします。
案3 Model にわける
下記のように HogeRepository
というのにわけてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
struct HogeRepository { let modelContext: ModelContext func fetchPiyo1HogeList() -> [Hoge] { return (try? modelContext.fetch( FetchDescriptor<Hoge>( predicate: #Predicate { $0.piyo == 1 } ) )) ?? [] } func addHoge() { modelContext.insert(Hoge(fuga: "fuga", piyo: 1)) } } struct ContentView: View { let hogeRepository: HogeRepository init(modelContext: ModelContext) { hogeRepository = .init(modelContext: modelContext) } var body: some View { Text(hogeRepository.fetchPiyo1HogeList().first!.fuga) } } |
テストはこんな感じです。
1 2 3 4 5 6 7 8 9 |
@MainActor @Test func filterPiyo1HogeList() { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Hoge.self, configurations: config) let repository = HogeRepository(modelContext: container.mainContext) repository.addHoge() let items = repository.fetchPiyo1HogeList() #expect(items.count == 1) } |
これはまあ案2とほぼ同じ感じですね。
案4 ModelContext を拡張する
SwiftData 使ってるんだからわざわざ ViewModel や Model つくって View から剥がしたくないということで 4 つ目の案です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct ContentView: View { @Environment(\.modelContext) private var modelContext var body: some View { Text(modelContext.fetchPiyo1HogeList().first!.fuga) } } extension ModelContext { func fetchPiyo1HogeList() -> [Hoge] { return (try? fetch( FetchDescriptor<Hoge>( predicate: #Predicate { $0.piyo == 1 } ) )) ?? [] } } |
テストはこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct PiyoSampleTests { @MainActor @Test func filterPiyo1HogeList() { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Hoge.self, configurations: config) let context = container.mainContext context.addHoge() let items = context.fetchPiyo1HogeList() #expect(items.count == 1) } } fileprivate extension ModelContext { func addHoge() { insert(Hoge(fuga: "fuga", piyo: 1)) } } |
addHoge
を fileprivate にすることでテスト用メソッドということが明確になりました。ただ SwiftData に格納するデータが増えると extension ModelContext
にどんどんメソッドが増えていくのでどうなんだろうという課題はあります。
追記(2024/11/25)
書いてから思いましたが案2、3は案4と組み合わせるともう少しよくなる気がしました。
こんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import Testing @testable import PiyoSample import SwiftData struct PiyoSampleTests { @MainActor @Test func filterPiyo1HogeList1() { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Hoge.self, configurations: config) let context = container.mainContext context.addHoge() let viewModel = Content2View.ViewModel(modelContext: context) let items = viewModel.fetchPiyo1HogeList() #expect(items.count == 1) } @MainActor @Test func filterPiyo1HogeList2() { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Hoge.self, configurations: config) let context = container.mainContext context.addHoge() let repository = HogeRepository(modelContext: context) let items = repository.fetchPiyo1HogeList() #expect(items.count == 1) } } fileprivate extension ModelContext { func addHoge() { insert(Hoge(fuga: "fuga", piyo: 1)) } } |
おわりに
SwiftData のテストを書くために 4 つ案を出してみましたが今のところ個人開発なら案4でいいかなという気持ちです。今回は SwiftData に焦点をあてて書きましたがそもそも SwiftUI 製アプリのテストってどう書くのがいいんだろう?
コメント