3

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!

Oletha
  • 7,324
  • 1
  • 26
  • 46
gbroekstg
  • 1,055
  • 1
  • 10
  • 19
  • Try `@objc var y: Int = 0`. – Willeke Oct 13 '20 at 11:53
  • Thx @Willeke, that works! Well, for a class at least. (I also need it for a struct and I can't use '@objc' there.) What's more is that I have no access to the class that has the property that I want to observe, so I cannot just add '@objc' to a var there. (The example that I provided was just to demonstrate the problem; in reality, it is a bit more complicated.) Anyway, I still don't really understand what the problem is. In all the examples that I find on Stack Overflow, people are using property 'exists' of class XCUIElement and the same code that I am using. So why doesn't my code work?! – gbroekstg Oct 13 '20 at 15:14
  • `NSPredicate(format: "y == 1")` uses KVC, `value(forKey: "y")`. – Willeke Oct 13 '20 at 15:49
  • Hi @Willeke, thank you for your suggestion, but can you please elaborate a little? I always get the "object is not KVC-compliant for the "y" property" error message. How do you suggest I change my predicate for this to work? – gbroekstg Oct 14 '20 at 07:19
  • I'm not familiar with the `XCTest` framework. My guess would be `NSPredicate(block:)` and/or setting `expectation.handler`. – Willeke Oct 14 '20 at 08:25

1 Answers1

2

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...

gbroekstg
  • 1,055
  • 1
  • 10
  • 19
  • `NSPredicate(format: "y == 1")` creates a `NSComparisonPredicate`. The `leftExpression` of the predicate is a [key path expression](https://developer.apple.com/documentation/foundation/nsexpression/1408892-init). `predicate.evaluate(with:)` calls `leftExpression.expressionValue(with:context:)` which calls `value(forKey: "y")`. – Willeke Oct 15 '20 at 23:06