问题描述
为什么这段代码如此执行?请注意测试代码中的注释,该注释指示哪些行通过和失败。
更具体地说,RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01))
如何在那里等待,同时仍然允许dispatchWorkItem
,{ [weak self] in self?.name = newName }
处理?如果线程正在运行循环中等待,线程如何处理任何工作项?
(如果问题没有道理,请更正我的理解)。
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()
}
解决方法
while
循环使执行无法继续进行,但是run
命令不仅等待,还处理该线程的运行循环中的事件,包括GCD源,计时器,分派的处理块等。
FWIW,在处理异步方法时,您可以:
-
使用完成处理程序。
通常,如果您使用异步方法,则为了推断对象的状态(例如何时关闭微调器,让用户知道何时完成),可以提供完成处理程序。 (这是假设简单的
async
是对一些更复杂的异步模式的简化。)如果您真的想使用一种异步方法来异步更改对象,并且您的应用当前不需要知道何时完成,那么请将该完成处理程序设为可选:
func updateName(to name: String,completion: (() -> Void)? = nil) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.name = name completion?() } }
然后,您可以在单元测试中使用期望值,这是测试异步方法的标准方法:
func testUpdateName() { let e = expectation(description: "Person.updateName") let person = Person() person.updateName(to: "Bob") { e.fulfill() } waitForExpectations(timeout: 1) XCTAssertEqual(person.name,"Bob") }
-
使用“阅读器”。
上一点是关于测试异步方法的一般观察。但是,如果您确实有一个异步变异对象的方法,则通常不会直接公开变异属性,而是可以使用“读取器”方法以一般的线程安全方式获取属性值。 (例如,在读写器模式下,您可能会异步更新,但您的读者将等待所有未完成的写入先完成。)
因此,请考虑使用读写器模式的
Person
: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 } } }
然后测试将使用
readName
func testUpdateName() { let person = Person() person.writeName(to: "Bob") let name = person.readName() XCTAssertEqual(name,"Bob") }
但是,如果没有某种同步读取的方法,通常也不会具有异步写入的属性。如果仅从主线程使用,则问题中的示例将起作用。否则,您将有比赛条件。