iosswiftxctestxctestcasexctestexpectation

How to test method that dispatch work asynchronously in Swift


I need to write the Unit Test for the following method

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

Solution

  • 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")
    }