Section 13/181 menit
13. Testing SwiftData: In-Memory dan Mock Strategies
13. Testing SwiftData: In-Memory dan Mock Strategies
In-Memory ModelContainer untuk Testing
swift
import Testing
import SwiftData
// Helper untuk buat container test yang terisolasi
@MainActor
func makeTestContainer(for types: any PersistentModel.Type...) throws -> ModelContainer {
let schema = Schema(types)
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true // ← Kunci: in-memory, tidak sentuh disk
)
return try ModelContainer(for: schema, configurations: config)
}
// Test dengan Swift Testing framework
@Suite("Task Repository Tests")
struct TaskRepositoryTests {
@Test("Create task berhasil menyimpan ke store")
@MainActor
func testCreateTask() async throws {
let container = try makeTestContainer(for: Task.self)
let context = container.mainContext
// Act
let task = Task(title: "Test Task", priority: "high")
context.insert(task)
try context.save()
// Assert
let descriptor = FetchDescriptor<Task>()
let tasks = try context.fetch(descriptor)
#expect(tasks.count == 1)
#expect(tasks.first?.title == "Test Task")
}
@Test("Unique constraint mencegah duplikat")
@MainActor
func testUniqueConstraint() async throws {
let container = try makeTestContainer(for: Article.self)
let context = container.mainContext
// Insert pertama
context.insert(Article(remoteID: "abc123", title: "Original", body: "..."))
try context.save()
// Insert duplikat — seharusnya upsert
context.insert(Article(remoteID: "abc123", title: "Updated", body: "..."))
try context.save()
// Harus tetap 1 record, tapi title terupdate
let all = try context.fetch(FetchDescriptor<Article>())
#expect(all.count == 1)
#expect(all.first?.title == "Updated")
}
@Test("Delete cascade menghapus children")
@MainActor
func testCascadeDelete() async throws {
let container = try makeTestContainer(for: Author.self, Post.self)
let context = container.mainContext
// Setup
let author = Author(name: "Penulis")
let post1 = Post(title: "Artikel 1", body: "...")
let post2 = Post(title: "Artikel 2", body: "...")
author.posts = [post1, post2]
context.insert(author)
try context.save()
// Act: hapus author
context.delete(author)
try context.save()
// Assert: posts juga terhapus (cascade)
let remainingPosts = try context.fetch(FetchDescriptor<Post>())
#expect(remainingPosts.isEmpty)
}
@Test("Migration V1 ke V2 berhasil split nama")
func testSchemaV1toV2Migration() async throws {
// Buat store dengan V1
let v1Container = try ModelContainer(
for: SchemaV1.User.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: false,
url: URL.temporaryDirectory.appending(path: "test-v1.store"))
)
let v1Context = v1Container.mainContext
let user = SchemaV1.User(name: "Budi Santoso", email: "budi@example.com")
v1Context.insert(user)
try v1Context.save()
// Migrate ke V2
let v2Container = try ModelContainer(
for: SchemaV2.User.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: false,
url: URL.temporaryDirectory.appending(path: "test-v1.store"))
)
let v2Context = v2Container.mainContext
let migratedUsers = try v2Context.fetch(FetchDescriptor<SchemaV2.User>())
#expect(migratedUsers.count == 1)
#expect(migratedUsers.first?.firstName == "Budi")
#expect(migratedUsers.first?.lastName == "Santoso")
// Cleanup
try FileManager.default.removeItem(at: URL.temporaryDirectory.appending(path: "test-v1.store"))
}
}
Dependency Injection untuk Testability
swift
// Protocol untuk testability
protocol TaskRepositoryProtocol {
func fetchAll() throws -> [Task]
func create(title: String, priority: String) throws -> Task
func delete(_ task: Task) throws
}
// Implementasi produksi
final class SwiftDataTaskRepository: TaskRepositoryProtocol {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetchAll() throws -> [Task] {
try modelContext.fetch(FetchDescriptor<Task>())
}
func create(title: String, priority: String) throws -> Task {
let task = Task(title: title, priority: priority)
modelContext.insert(task)
try modelContext.save()
return task
}
func delete(_ task: Task) throws {
modelContext.delete(task)
try modelContext.save()
}
}
// Mock untuk testing ViewModel tanpa database
final class MockTaskRepository: TaskRepositoryProtocol {
var tasks: [Task] = []
var shouldThrow = false
func fetchAll() throws -> [Task] {
if shouldThrow { throw MockError.fetchFailed }
return tasks
}
func create(title: String, priority: String) throws -> Task {
if shouldThrow { throw MockError.saveFailed }
let task = Task(title: title, priority: priority)
tasks.append(task)
return task
}
func delete(_ task: Task) throws {
tasks.removeAll { $0.persistentModelID == task.persistentModelID }
}
}
enum MockError: Error {
case fetchFailed, saveFailed
}