Why Mocking Matters in iOS Unit Testing

Asilbek Djamaldinov
11 min readOct 18, 2023

--

Here are my other articles of the Testing Flow in iOS :

  1. Testing in iOS: From Zero to Hero!
  2. Unlocking the Power of UI Testing in iOS Development
  3. Why Mocking Matters in iOS Unit Testing (Current)
  4. iOS Dev Must-Have: Snapshot Testing
  5. Why to Unit Test in Async Mode

Imagine you’re building an iOS app, and it needs to fetch data from the internet, which takes a long 5 seconds each time you test. But, in your app, there are many places where this happens. Now, when you want to test your code, waiting for these slow downloads can be frustrating. To avoid this, we use a cool trick called mocking.

Mocking means we fake the internet service and make our tests run super fast on our own computer. It’s like playing with toy data instead of waiting for the real stuff. So, you can quickly check if your code works without getting bored waiting for the internet. It’s like a shortcut to happy and speedy testing!

In this article, we’ll talk about why pretending or “mocking” is a big deal when testing stuff in iOS apps. So, let’s dive into why this is so awesome! 😄

What is Mocking?

Mocking is a testing technique used to isolate and evaluate individual components of your code. In iOS development, this typically means creating mock objects to stand in for real objects or dependencies that your code interacts with. These mock objects mimic the behaviour of the real ones but allow you to control and observe interactions during testing.

The cool part is we can watch and control what happens when we test our code. It’s like having a test double that does what we want.

The Need for Mocking

Isolation and Control

Unit tests are designed to focus on the behavior of a single unit or component of your code, such as a function, method, or class. Mocking is essential for isolating this unit from its dependencies. By replacing these dependencies with mock objects, you can precisely control the conditions under which your unit is tested. This isolation ensures that any failures are due to issues within the unit itself and not its dependencies.

Predictable Testing

Mocking allows you to simulate various scenarios, edge cases, and error conditions. You can program these mock objects to produce specific responses, errors, or exceptions, ensuring that your code handles these situations correctly. This predictability is critical for verifying that your unit functions as expected under different circumstances.

Speed and Efficiency

Real dependencies, like network requests or databases, can be slow, unreliable, or costly to access during testing. By using mock objects, you eliminate the need to interact with these resources, making your unit tests faster and more efficient. This means you can run tests frequently during development without incurring the overhead of real-world dependencies.

How to Implement Mocking in iOS Unit Testing

Alright, enough talk! Let’s roll up our sleeves, dive into the code, and have some fun getting our hands a bit dirty. 😄

Code Safari: Let’s look what we have

We’ve got a button on the main screen that, when you press it, downloads a photo and shows it on the screen. To make it more exciting, we’ll even add a little nap for our app, making it sleep for 5 seconds!

class NetworkService {
func loooongNetworkCall(_ completion: @escaping (Result<UIImage, Error>) -> ()) {
sleep(5)

// Open source photo from https://unsplash.com
let request = URLRequest(url: URL(string: "https://unsplash.com/photos/XVoyX7l9ocY/download?ixid=M3wxMjA3fDB8MXxzZWFyY2h8OHx8c2NvdGxhbmR8ZW58MHx8fHwxNjk3NTA5MTQ4fDA&force=true&w=640")!)
let task = URLSession.shared.dataTask(with: request) { data, _, error in
if let data, let image = UIImage(data: data) {
completion(.success(image))
} else if let error {
completion(.failure(error))
}
}

task.resume()
}
}

This piece of code is like a recipe for your app to do something special. It’s all about getting a picture from the internet and showing it on your screen. But, to make things interesting, it pretends to take a little nap for 5 seconds.

Here’s what’s happening:

  1. We have a function called loooongNetworkCall, which means it's going to take some time to finish. It needs a special helper (the completion handler) to tell us when it's done.
  2. The code first pretends to sleep for 5 seconds. It’s like your app is taking a quick snooze.
  3. Then, it sends a request to the internet to fetch a picture. The URL in the code tells it where to find the picture online.
  4. After a while, when it gets a response, it checks if everything went well. If it did, it gives us the picture. If something went wrong, it tells us there was an error.

So, this code is like a little adventure where our app takes a 5-second nap, goes on the internet to find a picture, and then either shows you the picture or says, “Oops, something didn’t go as planned!”

Today’s SUT (system under testing) is DeepOceanViewModel

class DeepOceanViewModel: ObservableObject {
@Published var uiImage: UIImage?
@Published var error: Error?

private let networkService = NetworkService()

func downloadWallpaper() {
networkService.loooongNetworkCall { [weak self] result in
guard let self else { return }

switch result {
case .success(let uiImage):
DispatchQueue.main.async {
self.uiImage = uiImage
}
case .failure(let error):
self.error = error
}
}
}
}

When you tap the button, it tells the app to download an image from the internet using the NetworkService. Once the download is complete, it will either display the image if it was successful or show an error message if something went wrong.

For testing purposes, we’ll create a pretend version of NetworkService to make sure everything works correctly without needing to access the internet. This way, we can check if the app behaves as expected when you press the button.

What is a plan?

Having a handy plan in your hand is like having a cheerful road map to guide you through your adventure. Let’s write our steps:

  1. We’ll begin by making a set of rules (a protocol) for both the real NetworkService and our pretend version called MockNetworkService.
  2. Then, we’ll change the way we set up the DeepOceanViewModel so we can easily put in the pretend service from the outside.
  3. Next, we’ll create our pretend MockNetworkService and teach it how to act.
  4. We’ll get busy writing tests to check if everything works as expected.
  5. And, when we’re done, we’ll tidy up and make sure everything is nice and clean.

This plan helps us make our code better and more reliable!

Step 1: Why we need a protocol?

We’re adding protocols to our app for a specific reason: so that both our real NetworkService and the pretend MockNetworkService speak the same language and follow the same rules. This way, we can switch between them without any trouble, like swapping parts in a toy set, thanks to a cool concept called “polymorphism” in programming.

protocol AnyNetworkService {
func loooongNetworkCall(_ completion: @escaping (Result<UIImage, Error>) -> ())
}

In this code, we’re defining a protocol called AnyNetworkService that sets the rules for any network service that wants to conform to it.

Here’s what it’s saying:

  • It requires a function called loooongNetworkCall that takes a completion handler as a parameter.
  • This loooongNetworkCall function should accept a closure that takes a Result object, which contains either an UIImage (if everything goes well) or an Error (if there's a problem).

By creating this protocol, we’re establishing a common set of rules that any network service must follow. This makes it easier to use different network services interchangeably in our code, as long as they adhere to these rules. It’s all about making our code flexible and consistent!

And eventually, we’ll make sure that our NetworkService follows the rules laid out in the AnyNetworkService protocol. In other words, NetworkService will promise to do what the protocol requires:

class NetworkService: AnyNetworkService {
...
}

Step 2: Injecting AnyNetworkService into DeepOceanViewModel

class DeepOceanViewModel: ObservableObject {
...

private let networkService: AnyNetworkService

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

...
}

In this updated DeepOceanViewModel code, we're introducing a new way to set up the networkService by accepting it as a parameter in the view model's constructor (init method). Here's what's happening:

  1. We declare a private property networkService of the type AnyNetworkService. This means we can use any object that conforms to the AnyNetworkService protocol as our network service.
  2. In the constructor (init), we require the caller to provide an object that follows the AnyNetworkService protocol as an argument. This is often referred to as "dependency injection." By doing this, we make it flexible to use either the real network service or a mock version (like MockNetworkService) depending on the context.

This change allows us to easily switch between different network services while creating the DeepOceanViewModel.

Step 3: Crafting MockNetworkService Behaviour

In this chapter, we’re diving into the world of creating a “pretend” version of our network service, known as the MockNetworkService. The goal here is to make it act just like a real network service but under our complete control. Why do we need this? Well, it's all about making testing easier and more efficient.

We’ll be wearing our developer hats and teaching the MockNetworkService how to respond to our requests. For example, we'll make it return images or errors as if they're coming from a real network. This way, we can mimic different scenarios and see how our app behaves, all without waiting for the real internet.

Think of it as setting up a virtual playground where we can create various situations and test how our app handles them. So, let’s roll up our sleeves and start crafting the behavior of our pretend network service!

To begin, open your unit test directory and create a new folder called “MockServices.” Inside this folder, create a Swift file named “MockNetworkService”. Here’s what it should look like:

And replace code inside with the following:

import XCTest
@testable import MyOceanTests

final class MyOceanTestsTests: XCTestCase {
// 1
var sut: DeepOceanViewModel!
var mockNetworkService: MockNetworkService!

override func setUp() {
super.setUp()

// 2
mockNetworkService = MockNetworkService()
sut = DeepOceanViewModel(networkService: mockNetworkService)
}

override func tearDown() {
// 3
sut = nil
mockNetworkService = nil

super.tearDown()
}
}
  1. Here, we’re setting up our test case for DeepOceanViewModel. We declare two properties, sut (System Under Test, which is the view model we're testing) and mockNetworkService (our pretend network service).
  2. In the setUp method, which runs before each test, we initialize our mockNetworkService by creating an instance of MockNetworkService. We also set up our sut (DeepOceanViewModel) with the mockNetworkService. This ensures that our view model uses the mock service during testing.
  3. The tearDown method, which runs after each test, is used to clean up resources. Here, we release references to sutand mockNetworkService to ensure they don't interfere with subsequent tests.

This setup allows us to create a clean environment for each test case, ensuring that any changes made during one test won’t impact the next test. It’s an essential part of unit testing to maintain consistency and isolate test cases.

Step 4: Testing Download Photo Functionality

At the end of your unit test class, add this test case:

// 1
func test_DeepOceanViewModel_downloadUserPhoto_didGetImage() {
// given
let expectation = XCTestExpectation(description: #function)

// when
sut.downloadUserPhoto()

let listener = sut.$uiImage.dropFirst().sink { _ in
expectation.fulfill()
}

wait(for: [expectation], timeout: 3)

// then
XCTAssertNotNil(sut.uiImage)
}

You don’t need to focus too much on the expectation and wait lines here. These lines are one of the ways to test asynchronous behaviour, and we’ll dive deeper into this topic in future articles.

Let’s break down the code:

  1. We’re starting a test for the DeepOceanViewModel function called downloadUserPhoto(). This test checks whether the view model can successfully obtain a user's photo.
  2. In “given” section, we set up an expectation using XCTestExpectation. This expectation is like a promise that the test will wait for something to happen before it finishes. We give it a description based on the test function's name.
  3. In the “when” section, we call sut.downloadUserPhoto(), which simulates the action of downloading a user's photo.
  4. We create a listener using the $uiImage property of sut. When it emit changes, we fulfill our expectation.
  5. We’re using wait(for:timeout:) to wait for the expectation to come true. Specifically, we're waiting for a maximum of 3 seconds to see if the image gets downloaded. If nothing happens within 3 seconds, we consider the test as failed.
  6. In the “then” section, we make an assertion using XCTAssertNotNil to check if the uiImage property of sut is not nil. If it's not nil, the test passes, indicating that we were able to download an image successfully.

This test verifies whether the DeepOceanViewModel can indeed download a user's photo and whether the uiImage property is updated as expected.

Hooray! We’ve done it! We’ve cleverly pretended with our network stuff to speed up our testing. Try it out, and feel proud of your fantastic work. You’ve done an awesome job! 🎉

Step 5: Wrapping Up and Cleaning

You might be wondering, “Hey, Asilbek, we’ve tested the successful scenario, but what about testing when things go wrong?” That’s a great question, and I’m glad you’re thinking along with me. The cool thing is, we’re in charge of our pretend network, so we can use our special powers to make it misbehave for our tests. 😄

What we’re planning is to make a special variable inside our mock network service that we can adjust. Depending on this variable, we’ll make the mock service behave differently and return the right result to test different cases.

Change your mockNetworkService :

class MockNetworkService: AnyNetworkService {
var isSuccessCase: Bool = true

func loooongNetworkCall(_ completion: @escaping (Result<UIImage, Error>) -> ()) {
if isSuccessCase {
completion(.success(UIImage()))
} else {
completion(.failure(NetworkError.failure))
}
}
}

If the isSuccessCase is set to true, we'll make the mock return the successful scenario. If it's not true, we'll make it send an error instead.

Add new test case to your unit test class:

func test_DeepOceanViewModel_downloadUserPhoto_didGetError() {
// given
// 1
mockNetworkService.isSuccessCase = false
let expectation = XCTestExpectation(description: #function)

// when
sut.downloadUserPhoto()

// 2
let listener = sut.$error.sink { _ in
expectation.fulfill()
}

wait(for: [expectation], timeout: 3)

// then
// 3
XCTAssertNotNil(sut.error)
}

Let’s go through the test code step by step, based on the comment numbers:

  1. We’re starting a test to check how the DeepOceanViewModel behaves when it encounters an error during the download process. We set the isSuccessCase variable of the mockNetworkService to false. This indicates that we want the mock service to simulate a scenario where there's an error.
  2. In the “then” section, we check whether the error property of the view model (sut) is not nil. If it's not nil, the test passes. This indicates that the view model successfully detected and stored an error, as expected.

This test checks if the DeepOceanViewModel responds correctly when the mockNetworkService is set to return an error instead of a successful scenario during the download. It's essential to ensure that the view model handles error cases properly.

Conclusion

In a nutshell, mocking is a vital tool in iOS unit testing. It helps you break down and thoroughly examine different parts of your code. By using mock objects, you can make sure your code works correctly under different scenarios while keeping your tests fast and cost-effective. Incorporating mocking into your testing process is a smart approach that can result in more dependable and easier-to-maintain iOS applications. It’s a serious practice that serious developers should consider.

Find it a good read?

Recommend this post by clicking the 👏 button so other people can see it and let’s connect on LinkedIn asilbekdjamaldinov

--

--