Why does this code execute like this? Note the comments in the test code which indicate which lines pass and fail.
More specifically, how is it that RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01)) waits there, while still allowing for the DispatchWorkItem, { [weak self] in self?.name = newName }, to process? If the thread is waiting on the run loop, how can the thread process any work items?
(Or please correct my understanding if the question doesn't make sense).
class Person {
private(set) var name: String = ""
func updateName(to newName: String) {
DispatchQueue.main.async { [weak self] in self?.name = newName }
}
}
class PersonTests: XCTestCase {
func testUpdateName() {
let sut = Person()
sut.updateName(to: "Bob")
XCTAssertEqual(sut.name, "Bob") // Fails: `sut.name` is still `""`
assertEventually { sut.name == "Bob" } // Passes
}
}
func assertEventually(
timeout: TimeInterval = 1,
assertion: () -> Bool
) {
let timeoutDate = Date(timeIntervalSinceNow: timeout)
while Date() < timeoutDate {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01))
if assertion() == true { return }
}
XCTFail()
}
The
whileloop keeps execution from proceeding, but theruncommand doesn’t just wait, but rather it processes events on that thread’s run loop, including the processing of GCD sources, timers, dispatched blocks, etc.FWIW, when you're dealing with an asynchronous method, you would either:
Use completion handler.
Generally if you have an asynchronous method, in order to reason about the state of the object (e.g. when to dismiss a spinner letting the user know when it’s done), you'd supply a completion handler. (This is assuming that the simple
asyncwas a simplification of some more complicated asynchronous pattern.)If you really want to have an asynchronous method that asynchronously mutates the object and your app doesn’t currently need to know when it’s done, then make that completion handler optional:
Then you can use expectations in your unit tests, which is the standard way of testing asynchronous methods:
Use a “reader”.
The previous point is a general observation about testing asynchronous methods. But if you really have a method that is asynchronously mutating an object, you generally would not expose the mutating properties directly, but rather you might use a “reader” method to fetch the property value in a general, thread-safe manner. (E.g. in reader-writer pattern, you might update asynchronously, but your reader would wait for any pending writes to finish first.)
So, consider a
Personthat is using the reader-writer pattern:Then the test would use the
readNameBut you generally would not have a property with asynchronous writes without some way to synchronize reads, too. The example in the question would work if used from main thread only. Otherwise, you’d have race condition.