Using Protocols to Mock an API Service

April 15, 2021 • 8 min read

Using a mock when you are working on implementing, and maintaining an API can greatly increase production speed and save you headaches when it comes to core features you want to include.

Why use a mock?

When working with a mock API service, the implementations can vary. A good approach will have a clear separation of concerns and will make sure that the functionality is extended to the classes that need it. While there are many different approaches to managing this, we will cover just one below.

Protocols

The iOS ecosystem uses protocols throughout many of its required classes. Many internal helpers and delegation classes use protocols as a foundation for the core iOS experience. Applications also seem to use protocols along with the MVVM paradigm to easily maintain the testability of the code throughout its timeline.

The look and feel of how you design your application and its architecture can vary between development groups, but one thing is certain: keep it away from your ViewController.

A quick and dirty example of how we can use the protocols baked into swift would be something like this:

protocol ContentAPI {
    func fetchPigs(completion: @escaping (Bool, Pig?) -> Void)
}

This gives us an easy-to-call method that is independent of a class and will be the basis that you build your network calls on. For example, you might interact with this protocol by using it in your class functions by conforming to the protocol when you create your class. Below is an example of how you would use this protocol.

final class ActualClient: ContentAPI {
    func fetchPigs(completion: @escaping (Bool, Pig?) -> Void) {
        // Here is the pig data you requested!
    }
}

final class MockAPIClient: ContentAPI {
    func fetchPigs(completion: @escaping (Bool, Pig?) -> Void) {
          // Here is the mocked pig data you requested!
    }
}

The beauty of this approach is that now your application can bypass the need to make an actual network request, and can rather now simulate one on the device.

Out of the scope of this documentation, you can create a procedure to check when and how you want to use the implementation. Some might choose to instantiate the class in the View Controller (remember this is very bad - no!), others might choose to check for an argument that is passed in at runtime. Whatever your approach, by separating the concern of the API logic into a protocol, you are allowing yourself the ease to split your actual client API and mock API for further, and easier development in the future.

How to use a mock?

Now comes the exciting stuff: mocking the data that we need without calling the server. In this example, we’ll tap into our inner farmer and request the JSON response from our local server in the hopes that our pig data will find its way back to us.

Mocking JSON

Once you have created your data (whether this be through a struct, a giant array, or even a JSON file), you are ready to interact with it. In our example below we are going to be mocking the data with JSON, and using a parser to easily read and modify the data - need be.

final class MockAPIClient: ContentAPI {
    func fetchPigs(completion: @escaping (Bool, Pig?) -> Void) {
          let filePath = "pigs"
          MockAPIClient.loadJsonDataFromFile(filePath, completion: { data in
              if let json = data {
                  do {
                      let pigs = try JSONDecoder().decode(Pig.self, from: json)
                      completion(true, pigs)
                  }
                  catch _ as NSError {
                      fatalError("Couldn't load data from \(filePath)")
                  }
              }
          })
    }
}

You can already see the server cooling down and thanking you for not overloading it with a request for the data about pigs. While this is just the JSON parsing approach, you can imagine how easy this would be to implement with something such as Codable and your structs that conform to it.

Simulating a slow network

One thing to keep in mind is that the speed at which the server responds with data is quite different than reading the file from your computer. For this very reason, our ContentAPI protocol comes in handy yet again and we can build on our previous MockAPIClient.

final class MockAPIClient: ContentAPI {
    private static let delay = 3
    
    func fetchPigs(completion: @escaping (Bool, Pig?) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(MockApiClient.delay)) {
            // Your data is now experiencing a delay. Standby for pig data.
        }
    }
}

Keep in mind, this is a simple way to delay the data. In most cases you’re mocking the data to test it quickly without delays, so you won’t find yourself implementing this in most cases. This is for the dreamers that want to show off their amazing animations as they wait for the data to return.

Network failure

Finally, as you suspect, no code is bug-free. You worked on your content all day, and finally, just when you thought you had it the server sends back an error that your request failed. Now, with mock data this doesn’t normally happen, mainly because it’s sitting in a file on your computer; however, in some cases, you might want to show an alert or maybe have something else happen in the case of an error and avoid a halt and catch fire situation. The fact that you extended your protocol to allow the mocking of the data made this so easy.

final class MockAPIClient: ContentAPI {
    func fetchPigs(completion: @escaping (Bool, Pig?) -> Void) {
        DispatchQueue.main.async {
            completion(false, nil)
        }
    }
}

Before you come for me with a pitchfork, yes I know - this is a generic and way to basic check for an error. I’ll leave it up to you to check for specific edge cases.

As you run that block of code you will find your app beautifully failing and come to a screeching stop as the Network Error alert prompt pops up notifying you of the network failure. This made our testing, and implementation of the feature easier by tenfold.

Where to use a mock?

Keep in mind that the data that you choose to mock is up to you. Most people might not own a dedicated server, and instead, rent space. This approach can rack up some heavy charges when you are just testing your code for development purposes. Use a mock when you want to reduce the server fatigue, or even want to test a newer version of an API that you haven’t launched to the public yet. Adjust your model, change your data (struct, array, or JSON), and fire the request away.

Conclusion

I hope that this was useful in getting you started using mocks with your iOS apps. Mocking data can be easy, and even fun once you get the hang of it. I avoided tests in this round to make sure that the focus stays on the implementation and use of mocks. In the future, I will write about testing and making sure that your mock is useful, and not just for show.