I am trying to unit test a custom UIView, which changes the UI asynchronously. This is the code for the custom view:
import UIKit
class DemoView: UIView {
    
    var label: UILabel!
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    
    func setup() {
        label = UILabel(frame: .zero)
        self.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        self.centerXAnchor.constraint(equalTo: label.centerXAnchor).isActive = true
        self.centerYAnchor.constraint(equalTo: label.centerYAnchor).isActive = true
    }
    
    @MainActor
    func setLabel(_ text: String) {
        Task {
            try await Task.sleep(for: .milliseconds(100))
            label.text = text
        }
    }
}
I want to test, that after calling the setLabel(_:) function, the text on the label did change, therefore I wrote the following test:
@MainActor
func testExample() async throws {
    let demoView = DemoView(frame: .zero)
    XCTAssertEqual(demoView.label.text, nil)
    
    demoView.setLabel("New Text")
    let expectLabelChange = expectation(for: NSPredicate(block: { _, _ in
        demoView.label.text != nil
    }), evaluatedWith: demoView.label)
    await waitForExpectations(timeout: 5)
    
    XCTAssertEqual(demoView.label.text, "New Text")
}
But the exception runs into a timeout and the assert fails. When I set breakpoints, I can see that the Task inside setLabel(_:) is executed, but never reenters after sleeping, even though the timeout is long enough. Only after the waitForExpectations finishes, the task inside setLabel(_:) is continued, however this is too late for the assert to catch the changes.
How can I write the test, so that the Task in setLabel(_:) continues?
NOTE: The code is adjusted for demonstrating the issue. In the real app I call an API instead of sleeping.
 
                        
You don't need
asynctesting for this, and you shouldn't use it.setLabelis notasync, and you're using an expectation! This test will pass (and I've rewritten a few minor things along the way):Even better, remove the
@MainActorfrom yoursetLabelcall; you can then remove it from the test function too.Under what circumstances would
asyncfor the test be appropriate? IfsetLabelwereasync! Suppose you rewritesetLabellike this:Now you need your tests to be
async— and now you don't need an expectation! Look how simple everything becomes: