Essential Developer

View Original

Testing code that uses DispatchQueue.main.async | iOS Lead Essentials Community Q&A

Watch on YouTube

In this episode, Caio replies to a question we received from Tulio in the private iOS Lead Essentials Slack community:

"How can I test code that dispatches work to the main DispatchQueue asynchronously? If I remove the thread handling code, my test succeeds."

To illustrate, imagine you need to test a View Controller that loads some text from a Service and renders it on a UILabel. The label should display the string "Loading…" until the service completes the request:

public class ViewController: UIViewController {
    @IBOutlet public var label: UILabel!
    private var service: Service!

    public override func viewDidLoad() {
        super.viewDidLoad()

        label.text = "Loading..."
        service.load { [weak self] text in
            self?.label.text = text
        }
    }
}

And here's the test:

func test_viewDidLoad_rendersStringFromService() {
    let service = ServiceSpy()
    let sut = ViewController.make(service: service)

    sut.loadViewIfNeeded()
    XCTAssertEqual(sut.label.text, "Loading...")

    service.completion?("a string")
    XCTAssertEqual(sut.label.text, "a string")
}

The test passes, but the production service completes in a background queue, and the UI needs to be updated in the main queue.

To solve this, you can dispatch the work to the main thread inside the service completion closure:

service.load { [weak self] text in
    DispatchQueue.main.async {
        self?.label.text = text
    }
}

But… Now the test fails because the assertion runs before the main.async block.

Watch now the full video to find out how to move threading logic away from the services and the UI using a Decorator, and how to deal with this threading challenge in legacy codebases where it's not easy to wrap services.

Subscribe to our YouTube channel and don't miss out on new episodes.

References