Hi everyone,
I’ve been working on testing a class that uses a @Published
property, where an async task is triggered by a method, and eventually, the published value is updated. Initially, I had trouble testing the asynchronous behavior because the test would fail unless the value update was synchronous. After some reflection, I realized the key issue: I wasn’t properly waiting for the publisher to send the updated value before making my assertion.
Here’s a simplified version of my code:
final class SUT {
@Published
private(set) var myImportantValue: String
func eventDidHappen() {
// Triggers an async task that eventually updates myImportantValue in a thread-safe way
// I avoid marking this as async to keep it transparent for the caller. The consumer just observes the published value.
}
}
The test outline looked like this:
@Test
func should_update_important_value_when_event_happens() async throws {
let sut = SUT()
var didPublish = false
sut.myImportantValue
.dropFirst()
.sink { _ in
didPublish = true
}
sut.eventDidHappen()
// Initially, I was expecting the following assertion to work, but it fails for async updates
#expect(didPublish) // Fails unless the change is synchronous
}
The problem was that I didn’t account for the fact that the value change happens asynchronously, and thus, I wasn’t giving the publisher enough time to emit the updated value. After some thought, I realized that in order to make the test reflect actual consumer behavior, I need to wait for the value change within the test, between triggering the event and asserting the result.
I missed the point that the proper approach would be to properly wait for the change in the sink before asserting with #expect(didPublish)
.
To solve this, I created a custom TestExpectation
to wait for the event to be triggered:
@Test
func should_update_important_value_when_event_happens() async throws {
let sut = SUT()
var didPublish = TestExpectation()
sut.myImportantValue
.dropFirst()
.sink { _ in
didPublish.fulfill()
}
sut.eventDidHappen()
try await fulfillment(of: didPublish, timeout: 1)
#expect(didPublish)
}
I derived this from how I formerly implemented such tests using XCTest
framework with its XCTExpectation
, hence the similarity.
The solution with the custom implementaion works well, but I also wanted to check if there’s anything in the Swift Testing framework that could simplify this.
Looking forward to hearing your thoughts!