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 while
loop keeps execution from proceeding, but the run
command 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 async
was 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:
func updateName(to name: String, completion: (() -> Void)? = nil) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.name = name
completion?()
}
}
Then you can use expectations in your unit tests, which is the standard way of testing asynchronous methods:
func testUpdateName() {
let e = expectation(description: "Person.updateName")
let person = Person()
person.updateName(to: "Bob") {
e.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(person.name, "Bob")
}
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 Person
that is using the reader-writer pattern:
class Person {
// don't expose the name at all
private var name: String = ""
// private synchronization reader-writer queue
private let queue = DispatchQueue(label: "person.readerwriter", attributes: .concurrent)
// perform writes asynchronously with a barrier
func writeName(to name: String) {
queue.async(flags: .barrier) {
self.name = name
}
}
// perform reads synchronously (concurrently with respect to other reads, but synchronized with any writes)
func readName() -> String {
return queue.sync {
return name
}
}
}
Then the test would use the readName
func testUpdateName() {
let person = Person()
person.writeName(to: "Bob")
let name = person.readName()
XCTAssertEqual(name, "Bob")
}
But 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.