How to test method that dispatch work asynchronously in Swift

1.4k views Asked by At

I need to write the Unit Test for the following method

func setLabelText(msg: String) {
     DispatchQueue.main.async {
          self.label.text = msg
      }
  }
3

There are 3 answers

0
Jon Reid On BEST ANSWER

Let's assume your test set-up already creates a view controller and calls loadViewIfNeeded() to connect any outlets. (This is from chapter 5, "Load View Controllers.") And that this view controller is in a property I'll name sut (meaning System Under Test).

If you write a test case to call setLabelText(msg:), then immediately check the view controller's label, this won't work.

If you had a dispatch to a background thread, then we'd need the test to wait for the thread to complete. But that's not the case for this code.

Your production code calls setLabelText(msg:) from the background. But test code runs on the main thread. Since it's already on the main thread, all we need to do is execute the run loop one more time. You can express this with a helper function which I introduce in chapter 10, "Testing Navigation Between Screens:"

func executeRunLoop() {
    RunLoop.current.run(until: Date())
}

With that, here's a test that works:

func test_setLabelText_thenExecutingMainRunLoop_shouldUpdateLabel() throws {
    sut.setLabelText(msg: "TEST")
    executeRunLoop()
    
    XCTAssertEqual(sut.label.text, "TEST")
}

This successfully tests the method, and completes quickly. But what if another programmer comes along and changes setLabelText(msg:), pulling the self.label.text = msg call outside the DispatchQueue.main.async? I describe this problem in chapter 13, "Testing Network Responses (and Closures)," in a section called "Keep Asynchronous Code in Its Closure." Basically, we want to test that the label doesn't change when the dispatched closure isn't run. We can do that with a second test:

func test_setLabelText_withoutExecutingMainRunLoop_shouldNotUpdateLabel() throws {
    sut.label.text = "123"
    
    sut.setLabelText(msg: "TEST")
    
    XCTAssertEqual(sut.label.text, "123")
}
0
nishith Singh On

Hello in my opinion using XCTestExpectation could benefit as this api was intended to test asynchronous operations(You can read more about it here)

As far as comparison between the two is concerned the only thing that I could think of is that with XCTestExpectation you will be able to test server timeouts(Say your api does not respond in expected time. URLSession has a default time out of 60 seconds) with specific error code and message.

0
RealNmae On

You can add completion closure to displayMessage and call expectation.fulfill() in test. Another totally different approach is to implement some presentation design pattern like coordinator or presenter. In that case all your UI presentation will be abstracted to non async methods.