The fact that refresh
detaches some async code to do its job, is an implementation detail, and your tests should not care about the implementation details.
Instead, focus on the behaviour of the unit. For example, in the scenario you posted, the expected behaviour is that sometime after refresh
is called, owner.data
should become "test". This is what you should assert against.
Your current test code follows the above good practice, only that, as you observed, it fails because it doesn't wait until the property ends up being set. So, try to fix this, but without caring how the async part is implemented. This will make your tests more robust, and your code easier to refactor.
One possible approach for validating the async update is to use a custom XCTestExpectation
:
final class PropertyExpectation<T: AnyObject, V: Equatable>: XCTNSPredicateExpectation {
init(object: T, keyPath: KeyPath<T, V>, expectedValue: V) {
let predicate = NSPredicate(block: { _, _ in
return object[keyPath: keyPath] == expectedValue
})
super.init(predicate: predicate, object: nil)
}
}
func testRefresh() {
let exp = PropertyExpectation(object: owner, keyPath: \.data, expectedValue: "test")
owner.refresh()
wait(for: [exp], timeout: 5)
}
Alternatively, you can use a 3rd party library that comes with support for async assertions, like Nimble:
func testRefresh() {
owner.refresh()
expect(self.owner.data).toEventually(equal("test"))
}
As a side note, since your code is multithreaded, strongly recommending to add some synchronization in place, in order to avoid data races. The idiomatic way in regards to the structured concurrency is to convert your class into an actor, however, depending on how you're consuming the class from other parts of the code, it might not be a trivial task. Regardless, you should fix the data races conditions sooner rather than later.