Swift Enum Mock Pattern (EMP)

Posted by Grego on April 7, 2022

Mocking in Swift?

Today I’ll talk about a technique I came up with that I’ve been using for the past few years with regards to testing and mocking in Swift. I call it the Swift Enum Mock Pattern or EMP for short (patent pending 🙃).

First, to set the level, let’s ask a few questions.

What is a mock?

A mock object is described as a:

… simulated object that mimics the behavior of a real object in controller ways, most often as part of a software testing initiative

For example, rather than having to depend on an HttpClient class’s methods for testing which rely on making REAL http calls out to the wild, wild web (WWW) – which are out of our control – a mock object (MockHttpClient) would be able to simulate a real http call and give us controlled results in its place.

Migrating an existing class for test

Extract and implement an interface (protocol)

Let’s look at our existing sample NetworkClient:

enum NetworkClient {
  /// Reusable URLSession for making requests
  private static let session: URLSession = {
    URLSession(configuration: .default)
  }()

  typealias Response<T: Decodable> = (T?, Error?) -> Void
  static func fetch<T: Decodable>(urlString: String, completion: @escaping Response<T>) {
    // performs real code to fetch data and deserialize from web service
  }

  // ...
}

Note: Implementation details are left out here for brevity.

Luckily swift protocols support static methods. First, we extract our public interface into its own protocol:

// 1. Create new protocol
protocol Networkable {
  // 2. hoist our Response typealias up since it's coupled to our `fetch` method
  typealias Response<T: Decodable> = (T?, Error?) -> Void
  // 3. Extract `fetch(urlString:completion:)`
  static func fetch<T: Decodable>(urlString: String, completion: @escaping Response<T>)
}

Make NetworkClient conform to Networkable:

enum NetworkClient: Networkable {
  // ...
}

Inject protocol

Then where we would normally use NetworkClient:

enum DogPhotoClient {
  static let dogUrl = "https://dog.ceo/api/breeds/image/random"

  /// Requests random dog photos from dog photo service
  static func fetchDogPhoto(completion: @escaping (DogPhoto?, Error?) -> Void) {
    // 1. this is where we use `NetworkClient`
    NetworkClient.fetch(urlString: Self.dogUrl,
                        completion: completion)
  }

  // ...
}

We would inject a Networkable, store it as a field and use that instead.

🤔 But enums can’t store fields, so we’ll have to convert it to a class first, and update our call sites:

// 1. enum becomes a class
final class DogPhotoClient {
  private static let dogUrl = "https://dog.ceo/api/breeds/image/random"

  // 2. create a standard drop-in replacement for our average case scenario, this is what our consumers will use 99% of the time.
  static let standard = DogPhotoClient(client: NetworkClient.self)

  // 3. add `client` field to store Networkable
  // 4. Since `NetworkClient` is a non-instantiable object (enum), we use Networkable.Type to inject the type rather than an instance of it.
  private let client: Networkable.Type
  init(client: Networkable.Type) {
    self.client = client
  }

  /// Requests random dog photos from dog photo service
  func fetchDogPhoto(completion: @escaping (DogPhoto?, Error?) -> Void) {
    // 5. Instead of calling `NetworkClient.fetch` we use our `client` field
    self.client.fetch(urlString: Self.dogUrl,
                        completion: completion)
  }

  // ...
}

Update call sites

This breaking change will force us to update our call sites:

// 1. note the newly added `.standard`:
DogPhotoClient.standard.fetchDogPhoto { dogPhoto, error in
    print(">> Dog Photo: \(String(describing: dogPhoto))")
    dogPhoto?.write(toJson: File.Sample.dogFile)
}

Note: If we wanted this code to also be testable we would factor out our DogPhotoClient.standard to its own field that could also be injected in our test classes. For brevity we’ll focus just on testing our DogPhotoClient class.

Our DogPhotoClient is now testable.

Add unit tests

We can now add unit tests. Under our project’s Tests directory we’ll create a similar structure to our production code and add these folders to Xcode. I find the simplest way to create a nested directory structure is through the command line using mkdir:

  mkdir -p Core/Networking/DogPhoto

Create a CallStack helper class

The CallStack class is what powers our EMP under the hood. It’s a data structure that allows us to keep track of a list of calls to our mock object. The name call stack is admittedly a bit of a misnomer due to the fact that:

  1. It is not a stack - we have random access and do not implement any stack-like behavior
  2. It tracks calls made to our mock — not individual frames for a single method call

Create Tests/Core/CallStack.swift:

// 1. Create a CallStack class using a generic `Method` type to represent the methods we call
final class CallStack<Method: Equatable> {
  // 2. Create a list to track mock method calls
  private var callStack = [Method]()

  // 3. Expose a method to check our list of calls for a match and make an assertion
  func assertCallWasMade(
  to method: Method,
  file: StaticString = #file,
  line: UInt = #line) {
      XCTAssertTrue(
      callStack.contains(method),
      "Expected call to \(method), but no such call was made.\n",
      file: file,
      line: line)
  }

  // 4. Add a `call` method to track calls made on our mock object
  func call(method: Method) {
      callStack.append(method)
  }
}

Create a MockNetworkClient class

The class we’ll be mocking is our NetworkClient which our DogPhotoClient uses to make network calls. Things get funky since our NetworkClient is fully static, so we’ll have to add some static storage and a method to reset the state of our object.

Create Tests/Core/Networking/MockNetworkClient.swift:

// 1. Mock network client implements Networkable protocol
enum MockNetworkClient: Networkable {
  // 2. Track calls made to this object using a new `CallStack` structure (defined below)
  private static var callStack = CallStack<Method>()

  /// Resets the callStack.
  /// Call this in the `setUp` method of your test file to reset call stack state
  /// on every test case.
  static func setUp() {
    callStack = CallStack()
  }

  static func fetch<T>(
  urlString: String,
  completion: @escaping Response<T>) where T : Decodable {
    callStack.call(method: .fetch(urlString: urlString))
  }

  // 3. An enum to match this method's testable apis
  enum Method: Equatable {
    // 4. Add a case for each method we want to test
    // 5. Provide parameters as associated types if needed for testing
    case fetch(urlString: String)
  }

  // 6. Following the law of demeter, forward our method calls to hide implementation details
  static func assertCallWasMade(
  to method: Method,
  file: StaticString = #file,
  line: UInt = #line) {
    callStack.assertCallWasMade(to: method, file: file, line: line)
  }
}

On the outside our MockNetworkClient functions like our regular one, conforming to the same Networkable protocol. We provide a Method enum which has cases for every method in this class that we want to test. Following the Law of Demeter, we forward our method calls to the call stack while hiding it. This kind of information hiding or encapsulation allows us to swap out the implementation of this method later and can help migrate us to a different underlying CallStack-like data structure or utility in the future by keeping our code loosely coupled.

Test our subject

Now that all of our dependencies are set up, let’s create our Tests/Core/Networking/DogPhoto/DogPhotoClientTests class:

import Foundation
import XCTest

@testable import SurvivalSwift

// 1. Create new class using XCTestCase
final class DogPhotoClientTests: XCTestCase {
  // 2. The object we'll be testing in this class
  private var sutDogClient: DogPhotoClient!
  // 3. The real (expected) dogUrl
  private static let dogUrl = "https://dog.ceo/api/breeds/image/random"

  override func setUp() {
    // 4. Reset mock network object (since it's static and we can't just make a new one)
    MockNetworkClient.setUp()
    // 5. Create new test object for a clean state
    sutDogClient = DogPhotoClient(client: MockNetworkClient.self)
  }

  // 6. create a test method
  func test_fetchDogPhoto_callsFetchApi_withProperUrl() {
    // 7. call our test object's method
    sutDogClient.fetchDogPhoto { photo, error in
      // ignore result for this test
    }

    // 8. check mock object for call -- first using a bad string to make sure the test fails
    MockNetworkClient.assertCallWasMade(to: .fetch(urlString: "test failed"))
  }
}

Run the tests, and the tests should fail! After the test fails, replace "test failed" with Self.dogUrl and the tests should turn green. Allowing the test to fail first helps prove that our tests are actually doing something and not just passing even when they shouldn’t.

Printing the CallStack object

If we want to go the extra mile we can make some changes to allow our call stack to be printable.

  // 1. Add `CustomDebugStringConvertible` to the `Method` conformance requirements
  final class CallStack<Method: Equatable & CustomDebugStringConvertible> {
    // ...

    // 2. Create new `callStackString` computed property
    private var callStackString: String {
      callStack.reduce("") { partialResult, next in
        partialResult + "\n\t -> " + next.debugDescription
      }
    }

    // 3. Update `assertCallWasMade()` method to print callStack:
    func assertCallWasMade(
    to method: Method,
    file: StaticString = #file,
    line: UInt = #line) {
      XCTAssertTrue(
        callStack.contains(method),
        "Expected call to \(method), but no such call was made.\n"
        + "Call stack:" + callStackString,
        file: file,
        line: line)
    }

    // ...
  }

Then update our MockNetworkClient.Method enum to conform to CustomDebugStringConvertible:

// 1. Add CustomDebugStringConvertible conformance
enum Method: Equatable, CustomDebugStringConvertible {
  case fetch(urlString: String)

  // 2. Implement `debugDescription`
  var debugDescription: String {
    switch self {
      case .fetch(let urlString):
        return ".fetch(urlString: \"\(urlString)\")"
    }
  }
}

Change our Self.dogUrl in DogPhotoClientTests to "failed test" again to make the test red and run:

DogPhotoClientTests.swift:28: error: -[SurvivalSwiftTests.DogPhotoClientTests test_fetchDogPhoto_callsFetchApi] : XCTAssertTrue failed - Expected call to .fetch(urlString: "failed"), but no such call was made.
Call stack:
    -> .fetch(urlString: "https://dog.ceo/api/breeds/image/random")

The down side to adding debugDescription is that it makes a little more effort to create these enums.