Search

Mocking With Protocols in Swift

Jeremiah Jessel

6 min read

Dec 17, 2021

iOS

Mocking With Protocols in Swift

Let’s get right to it. You need to test your code, and you need to test it often. You do a lot of manual testing throughout the development process, find bugs, and fix them. While this can be very beneficial, it leaves much of the code untested. When I say untested, I mean untested by you. The code will be tested at some point, it just might be by one of your users. This is where writing automated unit tests comes in, however, it is often the last thing developers do, if at all. Where do you start? How do you make this class testable? Many of these challenges can be overcome by using protocols.

Regardless of your testing methods, when you are writing testable code there are important characteristics your code should adhere to: 

  • You need to have control over any inputs. This includes any and all inputs that your class acts on. 
  • You need visibility into the outputs. There needs to be a way to inspect the outputs generated by your code. Your unit tests will use the outputs to validate things are working as expected. 
  • There should be no hidden state. You should avoid relying on internal system state that can affect your code’s behavior later. 

Using Swift mock protocols can help to meet these characteristics while also allowing for dependencies.

Mocking with Protocols

Mocking is imitating something or someone’s behavior or actions. In automated tests, it is creating an object that conforms to the same behavior as the real object it is mocking. Many times the object you want to test has a dependency on an object that you have no control over. 

There are several ways to create iOS unit testing mock objects. One way is to subclass it. With this approach you can override all the methods you use in your code for easy testing, right? Wrong. Subclassing many of these objects comes with hidden difficulties. Here are a few.

  • Unknown state: you don’t know if your object has any shared owners, which can result in one of them mutating the expected state of your mock.
  • Unexpected behavior: A change in your superclass, or its superclass, can create unknown effects to your mock.
  • Some classes cannot be subclassed, like UIApplication.

Also, Swift structs are powerful and useful value types. Structs, however, cannot be subclassed. If subclassing is not an option, then how can the code be tested?

Protocols! In Swift, protocols are full-fledged types. This allows you to set properties using a protocol as its type. Using protocols for testing overcomes many of the difficulties that come with subclassing code you don’t own and the inability to subclass structs.

Swift Mocking Example

In the example, you have a class that interacts with the file system. The class has basic interactions with the file system, such as reading and deleting files. For now, the focus will be on deleting files. The file is represented by a struct called MediaFile which looks like this.

struct MediaFile {
    var name: String
    var path: URL
}

The FileInteraction struct is a convenience wrapper around the FileManager that allows easy deletion of the MediaFile

struct FileInteraction {
    func delete(_ mediaFile: MediaFile) throws -> () {
        try FileManager.default.removeItem(at: mediaFile.path)
    }
}

All of this is managed by the MediaManager class. This class keeps track of all of the users media files and provides a method for deleting all of the users media. deleteAll method returns true if all the files were deleted. Any files that are unable to be deleted are put back in the media array.

class MediaManager {
    let fileInteraction: FileInteraction = FileInteraction()
    var media: [MediaFile] = []
   
    func deleteAll() -> Bool {
        var unsuccessful: [MediaFile] = []
        var result = true
        for item in media {
            do {
                try fileInteraction.delete(item)
            } catch {
                unsuccessful.append(item)
                result = false
            }
        }
        media = unsuccessful
        return result
    }
}

This code, as it stands, is not very testable. It is possible to copy some files to the directory, create the MediaManager with MediaFiles that point to them, and run a test. This, however, is not repeatable or fast. A protocol can be used to make the tests fast and repeatable. The goal is to mock the FileInteraction struct without disrupting MediaManger. To do this, create a protocol with the delete method signature and declare the FileInteraction conformance to it.

protocol FileInteractionProtocol {
    func delete(_ mediaFile: MediaFile) throws -> ()
}

struct FileInteraction: FileInteractionProtocol {
    ...
}

There are two changes to MediaManager that need to be implemented. First, the type of the fileInteraction property needs to be changed. Second, add an init method that takes a fileInteraction property and give it a default value.

class MediaManager {
    let fileInteraction: FileInteractionProtocol
    var media: [MediaFile] = []
    
    init(_ fileInteraction: FileInteractionProtocol = FileInteraction()) {
        self.fileInteraction = fileInteraction
    }

    ...
}

Now MediaManager can be tested. To do so, a mock FileInteraction type will be needed.

struct MockFileInteraction: FileInteractionProtocol {
    func delete(_ mediaFile: MediaFile) throws {
        
    }
}

Now the test class can be created.

class MediaManagerTests: XCTestCase {
    var mediaManager: MediaManager!

    override func setUp() {
        mediaManager = MediaManager(fileInteraction: MockFileInteraction())

        let media = [
            MediaFile(name: "file 1", path: URL(string: "/")!),
            MediaFile(name: "file 2", path: URL(string: "/")!),
            MediaFile(name: "file 3", path: URL(string: "/")!),
            MediaFile(name: "file 4", path: URL(string: "/")!)
        ]
        
        mediaManager.media = media
    }

    func testDeleteAll() {
        mediaManager.deleteAll()
        XCTAssert(mediaManager.deleteAll(), "Could not delete all files")
        XCTAssert(mediaManager.media.count == 0, "Media array not cleared")
    }
}

All of this looks good, except the delete method is marked as throws but is never tested to throw. To do this, create another mock that throws exceptions.

struct MockFileInteractionException: FileInteractionProtocol {
    func delete(_ mediaFile: MediaFile) throws {
        throw Error.FileNotDeleted
    }
}

Then modify the test class.

class MediaManagerTests: XCTestCase {
    var mediaManager: MediaManager!
    var mediaManagerException: MediaManager!

    override func setUp() {
        mediaManager = MediaManager(fileInteraction: MockFileInteraction())
        mediaManagerException = MediaManager(fileInteraction: MockFileInteractionException())
        
        let media = [
            MediaFile(name: "file 1", path: URL(string: "/")!),
            MediaFile(name: "file 2", path: URL(string: "/")!),
            MediaFile(name: "file 3", path: URL(string: "/")!),
            MediaFile(name: "file 4", path: URL(string: "/")!)
        ]
        
        mediaManager.media = media
        mediaManagerException.media = media
    }
    
    func testDeleteAll() {
        XCTAssert(mediaManager.deleteAll(), "Could not delete all files")
        XCTAssert(mediaManager.media.count == 0, "Media array not cleared")
    }
    
    func testDeleteAllFailed() {
        XCTAssert(!mediaManagerException.deleteAll(), "Exception not thrown")
        XCTAssert(mediaManagerException.media.count > 0, "Media array was incorrectly cleared")
    }
}

Mocks, Dummies, Stubs, and Fakes

A Swift mock is only one type of “test double”–a test double being a replacement entity used solely for testing. There are four commonly used types of test double:

  • Mocks.
  • Dummies.
  • Stubs.
  • Fakes.

Dummy objects are empty objects used within a unit test. When you use a dummy object, you isolate the code being tested; while you can determine whether the code correctly calls the dummy, the dummy does nothing.

Stub objects are objects that always return a specific set of core data. For example, you could set an error-handling object to always return “true” if you wanted to test a failure condition, or drop a stub object into legacy code to test and isolate behavior.

Fake objects are objects that still roughly correlate to a “real” object, but are simplified for the sake of testing. Instead of pulling data from a database, you might return a specific set of data that could have been pulled from the database. Fake objects are similar to stubs, just with more complexity.

While there are use cases for dummies, stubs, and fakes–such as UI tests–mocks frequently provide more comprehensive Swift unit test data. Mocking protocols are particularly useful for dynamic environments and dependency injection. Use mocks for complex tasks such as integration tests.

Partial vs. Complete Mocking

One final element to discuss is the practice of partial mocking vs. complete mocking.

Complete mocking refers to mocking the entirety of the protocol, while partial mocking refers to when you override a specific behavior or behaviors in the protocol. 

When unit testing, software engineers use partial mocking to drill down to specific behaviors within the protocol. Otherwise, partial and complete mocking are virtually identical–the difference lies in scope. 

Summary

Initially the MediaManager delete all method was not very testable. Using a protocol to mock interaction with the file system made testing this code repeatable and fast. The same principles for testing the delete all method can be applied to other areas of interaction such as reading, updating, or moving files around. Mock protocols can also be used to mock Foundation classes such as URLSession and FileManager where applicable. 

In addition to mock protocols, there are also test suites, mocking libraries, and mocking frameworks, such as Mockingbird–and the automated testing provided by continuous integration/delivery before pushing production code. Nevertheless, you should still know how to hand code your mocking and develop your own tests.

Protocols are powerful tools for testing code–and testing should never be an afterthought. Learn more about high-quality, resilient code generation and test-driven development at our iOS and Swift Essentials bootcamp.

Zack Simon

Reviewer Big Nerd Ranch

Zack is an Experience Director on the Big Nerd Ranch design team and has worked on products for companies ranging from startups to Fortune 100s. Zack is passionate about customer experience strategy, helping designers grow in their career, and sharpening consulting and delivery practices.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News