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,