Discover how Mocks work in Swift
You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿
You’ve heard that Mocks are helpful to write tests, but you're not really sure how they work? 🤨
Don’t worry! I’ve got you covered! 😌
In just a few paragraphs we’ll go over everything you need to know to understand how this powerful tool works!
Let’s start with this ViewModel:
This ViewModel relies on a Service to fetch its data from the network.
But while this setup works great in production, it also makes it difficult to simulate errors or to write reliable tests for our ViewModel 😩
So how about we solve this by implementing a mocked version of our Service?
Let’s do it in 5 easy steps 🔥
Step 1️⃣, we abstract our Service behind a protocol and we make it injectable:
Step 2️⃣, we start implementing a MockedService that conforms to the protocol:
Step 3️⃣, we add a property that lets us customize the data that our MockedService will provide (notice how this will allow us to simulate both successes and failures!):
Step 4️⃣, we add another property to keep track of how many times the method getData has been called:
Step 5️⃣, we can now write reliable tests for our ViewModel using our MockedService:
And that’s it: we’ve successfully mocked the dependency of our ViewModel 🎉
As we’ve just seen, such a mock can be very useful when writing tests...
...but it can also prove helpful in other use cases: like populating SwiftUI previews or reproducing bugs!
Here’s the code if you want to experiment with it:
class Service: Servicing {
func getData(
_ completion: @escaping (Result<String, Error>) -> Void
) {
/* some networking code */
}
}
class ViewModel: ObservableObject {
@Published var data: String? = nil
@Published var error: Error? = nil
private let service: Servicing
init(service: Servicing) {
self.service = service
}
func fetchData() {
service.getData { [weak self] result in
switch result {
case .success(let data):
self?.data = data
case .failure(let error):
self?.error = error
}
}
}
}
class MockedService: Servicing {
var getDataCallCounter = 0
var result: Result<String, Error>!
func getData(
_ completion: @escaping (Result<String, Error>) -> Void
) {
getDataCallCounter += 1
completion(result)
}
}
final class ViewModelTests: XCTestCase {
func testSuccessCase() {
// Given
let mockedService = MockedService()
mockedService.result = .success("Hello, world!")
let viewModel = ViewModel(service: mockedService)
// When
viewModel.fetchData()
// Then
XCTAssertEqual(mockedService.getDataCallCounter, 1)
XCTAssertEqual(viewModel.data, "Hello, world!")
XCTAssertNil(viewModel.error)
}
}