Why do you need to Mock DispatchQueue in Swift on the iOS platform for unit testing

Why do you need to Mock DispatchQueue in Swift on the iOS platform for unit testing
This article is a part of unit testing series which will teach you how to write a testable code, create mocks and how to inject them during unit testing. Today, we are going to look at the use-case where you need to mock the DispatchQueue and inject the instance to facilitate testing

Why did I need to create a mock of DispatchQueue in the app?

A few months ago, I was working on a take-home exercise. I wrote the app in MVVM architecture and of course view model and view were different entities. In order to facilitate communication, I built a bridge between them.

For the context, here is the snapshot of my view model code.


final class AsyncOperationViewModel {
    let networkService: NetworkServiceable

    init(networkService: NetworkServiceable) {
        self.networkService = networkService
    }

    func loadData() {
        networkService.loadData(with: "https://testserver.com/userInfo") { [weak self] result in
            guard let self = self else { return }
            DispatchQueue.main.async {
                // Codd to Update the UI
                view.nameLabel.text = result.username
            }
        }
    }
}
I need to run the code on the main thread. Network service returns the closure on background thread and I need to transfer the control on main thread before applying any UI updates

Everything worked like a charm until I ran unit tests. For some reason, tests were ignoring the code executing inside the closure on the main dispatch queue. As it turns out, the code inside the main queue closure gets executed in an async manner. By the time test has completed its run, the code is yet to be executed, so the failure.

Solution

There are other ways to get around the problem. For example, you can treat it as an async test and use expectations to verify the result of the async operation. But that's a bit hacky since you have to keep guessing when the async block completes the execution. It might turn out to be unreliable if future async operations end up taking more time than the deliberate delay in tests.

Another option is to move the code to transfer execution on a main thread over to the view layer. Since we aren't (ideally) testing the view layer, we can ignore this jumping dance between async and sync modes. But it's up to you how you want to structure it.

The better option would be to inject the DispatchQueue instance and mocking it out during unit tests. That way, when the app runs, it will use the main queue instance, but when the test runs, it will just execute the dummy closure. This is enough since tests do not expect to run this block of code on the main thread.

We will do a couple of things,

  • Inject the DispatchQueue to the view model during initialization


final class AsyncOperationViewModel {

    let networkService: NetworkServiceable
    let dispatchQueue: DispatchQueueType

    init(networkService: NetworkServiceable, dispatchQueue: DispatchQueueType = DispatchQueue.main) {
        self.networkService = networkService
        self.dispatchQueue = dispatchQueue
    }

    func loadData() {
        networkService.loadData(with: "https://testserver.com/userInfo") { [weak self] result in
            guard let self = self else { return }
            self.dispatchQueue.async {
                // Codd to Update the UI
                view.nameLabel.text = result.username
            }
        }
    }
}
  • Create a DispatchQueue type

We will create a protocol named DispatchQueueType and add async method to it. It will take a closure named work as the parameter.

We will make DispatchQueue conform to DispatchQueueType and implement this method triggering the internal dispatch queue method


import Foundation

protocol DispatchQueueType {
    func async(execute work: @escaping @convention(block) () -> Void)
}

extension DispatchQueue: DispatchQueueType {
    func async(execute work: @escaping @convention(block) () -> Void) {
        async(group: nil, qos: .unspecified, flags: [], execute: work)
    }
}

  • Mocking DispatchQueue

This works great for the real app run. Now let's mock the DispatchQueue by creating a new class MockDispatchQueue which conforms to DispatchQueueType and can be injected into AsyncOperationViewModel during testing


final class MockDispatchQueue: DispatchQueueType {
    func async(execute work: @escaping @convention(block) () -> Void) {
        work()
    }
}

  • Testing AsyncOperationViewModel

Now that we are ready to write unit tests, let's create an instance of AsyncOperationViewModel with mocked network service and dispatch queue and write tests as usual.

Please note that due to mocked DispatchQueue type, we are now executing operation in synchronous mode what otherwise would've been asynchronous without mocking

func testThatViewModelCorrectlySetsUpNameLabelTitleAfterLoadingData() {

    let viewModel = AirlinesListViewModel(networkService: MockNetworkService(), dispatchQueue: MockDispatchQueue())

    let viewController = .......

    viewModel.view = viewController
    viewModel.loadData()
    XCTAssertEqual(viewController.nameLabel.text, "John Doe")

}

Source Code

The full source code for this tutorial is available on this GitHub Gist for further reference

Support and Feedback

If you have any comments or questions, please feel free to reach out to me on LinkedIn.

If you like my blog content and wish to keep me going, please consider donating on Buy Me a Coffee or Patreon. Help, in any form or amount, is highly appreciated and it's a big motivation to keep me writing more articles like this.

Consulting Services

I also provide a few consulting services on Topmate.io, and you can reach out to me there too. These services include,

  1. Let's Connect
  2. Resume Review
  3. 1:1 Mentorship
  4. Interview Preparation & Tips
  5. Conference Speaking
  6. Take-home Exercise Help (iOS)
  7. Career Guidance
  8. Mock Interview