-
This might be more of a Swift Concurrency question rather than a GRDB question, but I thought I'd try to get the communities advice. ContextI have a SwiftUI view that is powered by a view model which fetches & keeps data fresh using a
The ProblemsNow I'd like to write some tests for my view model using Swift Testing and am running into a few stumbling blocks: Kicking off the
|
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
Are you looking for this https://developer.apple.com/documentation/testing/testing-asynchronous-code? |
Beta Was this translation helpful? Give feedback.
-
We have to wait for something. So maybe "clunky" refers to
Testing async sequences is difficult because it is unclear when the iteration has started. There is a forum thread with a lot of discussions about this topic at Reliably testing code that adopts Swift Concurrency? I would highlight this particular answer. The general idea is to make the ViewModel testable. Good news: it is already, thanks to If SwiftUI knows that views should be updated, tests can know as well, and maybe this is exactly what the tests should do. Here is a rough sketch of such tests, one for the initial value, another for an update. import GRDB
import Observation
import Semaphore
@MainActor @Observable final class MyViewModel {
var playerCount = 0
private let reader: DatabaseReader
init(reader: DatabaseReader) {
self.reader = reader
}
func observeData() async {
let counts = ValueObservation
.tracking(Table("player").fetchCount)
.values(in: reader)
do {
for try await count in counts {
playerCount = count
}
} catch {
// Handle error
}
}
}
@Suite
struct MyTests {
let dbQueue: DatabaseQueue
init() throws {
dbQueue = try DatabaseQueue()
try dbQueue.write { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
}
try db.execute(sql: "INSERT INTO player DEFAULT VALUES")
}
}
@Test
@MainActor func wait_for_first_value() async throws {
let vm = MyViewModel(reader: dbQueue)
#expect(vm.playerCount == 0)
// Wait for first value
let semaphore = AsyncSemaphore(value: 0)
withObservationTracking {
_ = vm.playerCount
} onChange: {
semaphore.signal()
}
let task = Task {
await vm.observeData()
}
defer { task.cancel() }
await semaphore.wait()
// Proceed with assertions
#expect(vm.playerCount == 1)
}
@Test
@MainActor func wait_for_change() async throws {
let vm = MyViewModel(reader: dbQueue)
#expect(vm.playerCount == 0)
// Wait for first value
let semaphore = AsyncSemaphore(value: 0)
withObservationTracking {
_ = vm.playerCount
} onChange: {
semaphore.signal()
}
let task = Task {
await vm.observeData()
}
defer { task.cancel() }
await semaphore.wait()
// Wait for second value
withObservationTracking {
_ = vm.playerCount
} onChange: {
semaphore.signal()
}
try await dbQueue.write { db in
try db.execute(sql: "INSERT INTO player DEFAULT VALUES")
}
await semaphore.wait()
// Proceed with assertions
#expect(vm.playerCount == 2)
}
} I have reused @Test
@MainActor func wait_for_first_value() async throws {
let vm = MyViewModel(reader: dbQueue)
#expect(vm.playerCount == 0)
// Wait for first value
var task: Task<Void, Never>?
await withCheckedContinuation { continuation in
withObservationTracking {
_ = vm.playerCount
} onChange: {
continuation.resume()
}
task = Task {
await vm.observeData()
}
}
defer { task?.cancel() }
// Proceed with assertions
#expect(vm.playerCount == 1)
} |
Beta Was this translation helpful? Give feedback.
We have to wait for something. So maybe "clunky" refers to
await MainActor.run {}
. Indeed I'm not sure it guarantees with 100% certainty that the fresh values were notified and that you can proceed with assertions. And at some point in the futureviewModel.observeData()
could evolve and await for something else before observing the database, and break the tests.Testing async sequences is difficult because it is unclear when the iteration has started. There is a forum thread with a lot of discussions about this topic at Reliably testing cod…