XCTWaiter().wait with XCTestExpectation and NSPredicate seems to fail

1.7k views Asked by At

I am trying to write unit tests where I want my test case to wait for a variable in a certain class to change. So I create an expectation with a predicate and wait for the value to change using XCTWaiter().wait(for: [expectation], timeout: 2.0), which I assume is the correct method to use.

The following code works as expected:

class ExpectationTests: XCTestCase {
var x: Int = 0

private func start() {
    _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
        self.x = 1
    }
}

func test1() {
    let predicate = NSPredicate(format: "x == 1")
    let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
    start()
    let result = XCTWaiter().wait(for: [expectation], timeout: 2.0)
    switch result {
    case .completed:    XCTAssertEqual(x, 1)
    case .timedOut:     XCTFail()
    default:            XCTFail()
    }
}

A variable (x) is set to 0 and then changes to 1 after 0.5s by the start() function. The predicate waits for that var (x) to change. That works: result is set to .completed and the var actually is set to 1. Yeah :-)

However, when the variable that I want to observe is not a local var, but is in a class somewhere, it no longer works. Consider the following code fragment:

class MyClass: NSObject {
    var y: Int = 0
    
    func start() {
        _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
            self.y = 1
        }
    }
}

func test2() {
    let myClass = MyClass()
    let predicate = NSPredicate(format: "y == 1")
    let expectation = XCTNSPredicateExpectation(predicate: predicate, object: myClass)
    myClass.start()
    let result = XCTWaiter().wait(for: [expectation], timeout: 2.0)
    switch result {
    case .completed:    XCTAssertEqual(myClass.y, 1)
    case .timedOut:     XCTFail()
    default:            XCTFail()
    }
}

It is quite similar to the first piece of code, but this always ends after 2 seconds with result being .timedOut. I can't see what I am doing wrong. I use a variable from object myClass that I pass into the expectation instead of a local var and object 'self'. (The class var myClass.y is actually set to 1 when the test ends.)

I tried replacing XCTNSPredicateExpectation(predicate:object) with expectation(for:evaluatedWith:handler), but that didn't make any difference.

Many examples here on StackOverflow use a predicate that checks for exists in XCUIElement. But I am not testing UI; I just want to check if some var in some class has changed within a timeout period. I don't understand why that is so different from checking var exists in XCUIElement.

Any ideas?! Thank you in advance!

1

There are 1 answers

1
gbroekstg On

Well, thanks to @Willeke for pointing me in the right direction, I did find a solution, but I can't say I understand it completely... Here's what my code looks like now:

// MARK: - Test 2
    class MyClass: NSObject {
        var y: Int = 0
        
        func start() {
            _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
                self.y = 1
            }
        }
    }
    
    func test2() {
        let myClass = MyClass()
        let predicate = NSPredicate() { any, _ in
//            guard let myClass = any as? MyClass else { return false }
            return myClass.y == 1
        }
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: myClass)
        myClass.start()
        let result = XCTWaiter().wait(for: [expectation], timeout: 2.0)
        switch result {
        case .completed:    XCTAssertEqual(myClass.y, 1)
        case .timedOut:     XCTFail()
        default:            XCTFail()
        }
    }

I can use a predicate with a closure that regularly checks whether the var has changed and returns true if it has the correct value. (It does that about once per second.) However, I actually thought that's what XCTWaiter was for, given the description in the documentation of expectation(for:evaluatedWith:handler:) (which is a convenience method for XCTNSPredicateExpectation):

The expectation periodically evaluates the predicate. The test fulfills the expectation when the predicate evaluates to true.

So, I am happy that I can move on, but I still don't understand why this doesn't work with NSPredicate(format: "y == 1") instead of the predicate with the closure...