0

For testing purposes, I have the following test function:

func test_wait() {
    var string: String?
    DispatchQueue.main.async {
        string = "set"
        print("string set")
    }
    let notNilPredicate = NSPredicate(format: "self != nil")
    let notNilExpectation = expectation(for: notNilPredicate, evaluatedWith: string)
    print("start waiting")
    let waitResult = XCTWaiter.wait(for: [notNilExpectation], timeout: 5)
    XCTAssert(waitResult == .completed, "wait for notNilExpectation failed with result \(waitResult)")
}  

This test fails.
The wait result is .timedOut, and the log is

start waiting
string set
… : XCTAssertTrue failed - wait for notNilExpectation failed with result XCTWaiterResult(rawValue: 2)
Interrupting test  

I do not understand why the wait fails although var string is set.
However, the test succeeds when I out comment DispatchQueue.main.async, i.e. when I execute its block synchronously. The log is then

string set
start waiting
Test Case '-[ShopEasyTests.CoreDataCloudKitContainerTest test_wait]' passed (1.087 seconds).  

To my understanding, the async version of the test function should also work.
What is wrong?

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116

1 Answers1

1

It's the predicate (and the semantics of capture). You think you are handing a reference to string into your predicate evaluation, but you aren't; you are just passing nil, and nil is not going to magically be not-nil any time soon. To confuse yourself less, rewrite as

let notNilPredicate = NSPredicate {_,_ in string != nil }
let notNilExpectation = expectation(for: notNilPredicate, evaluatedWith: nil)
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Great! It works. But could you explain why? I would like to understand the situation... – Reinhard Männer Jul 13 '22 at 11:19
  • Edited to be a bit clearer. – matt Jul 13 '22 at 11:20
  • I am sorry, it is still not clear to me. My question is: Why does my async version fail, whereas my sync version succeeds, and why does your version in both cases succeed? Maybe I have oversimplified my example - my real problem is more complicated. – Reinhard Männer Jul 13 '22 at 12:26
  • Sorry, I don't know how to say it plainer. Your predicate doesn't do anything; you aren't actually waiting _for_ anything. You're just looking at the _value_ of the string once, right now. That value is a copy; it will never change. In your async code, it is nil and stays nil. In your normal code, it is set and stays set. In my code, I actually wait and watch the actual string _variable_, so when it changes, I see the change. – matt Jul 13 '22 at 12:44
  • So this has nothing to do with synchronous and asynchronous, really; it's that you don't know how write a predicate that watches for something. I showed you how to do it. – matt Jul 13 '22 at 12:48
  • Maybe I understand it now: My predicate is static, i.e. my expectation uses the current value at the time it is created, whereas your block-based predicate is dynamic, i.e. is evaluated each time XCTWaiter.wait checks the expectation. Did I get the point? – Reinhard Männer Jul 13 '22 at 13:56
  • Yes, exactly so. It is possible to write a boolean predicate expression like your not-nil predicate, but then `self`, the value passed into the evaluation, needs to be a _class_ instance so that we get a _reference_ to it, and then we can watch some property of that instance. But in your case, `string` is a struct, which is a _value_ type, so a frozen copy of its current value is passed into your predicate, and just sits there. I am assuming here that you understand the difference between a value type and a reference type. – matt Jul 13 '22 at 14:45
  • Now I got it! Thanks a lot. Learned something! – Reinhard Männer Jul 13 '22 at 14:47
  • In my world, unit tests that wait for a boolean expression are so common that we actually have a utility method that makes the formation of the block-based predicate and the expectation wrapped around it and the start of the wait a one-liner. – matt Jul 13 '22 at 14:57
  • Interesting! Do you mind to update your answer? I am sure it would help others. – Reinhard Männer Jul 13 '22 at 15:00
  • It's pretty obvious how to write it, and I think it would stray too far off the topic of the question that was actually asked. – matt Jul 13 '22 at 15:22