Writing unit tests with mocked dependencies in Swift
Tags: swift, ios, testing, programming
Published: 2022-11-09 17:33:06

When working on your code, no matter the language or framework you're working in, it's vital for the maintainability of your code to write tests. So, when I dove into the world of iOS app development a little over a year ago, I made sure to also figure out how to write tests for my code in Swift.

Having quite some experience with writing tests in .NET and PHP, I was familiar with the Moq library and Prophecy and how they're used to mock the dependencies of the class you're testing. I wanted something similar in Swift, so I set out to figure out how to do this.

## The start
First of all, let's start with a simple example of the code we want to test. The following example contains a very simple data manager object (the class we want to test), that has a dependency on an API client.

```swift
struct UserInfo {
    let name: String
}

class DataManager {
    private let client = ApiClient()

    public func getUserInfo() -> UserInfo {
        let name = client.getUserName()
        return UserInfo(name: name)
    }
}

class ApiClient {
    // Imagine a call to an API begin performed here
    public func getUserName() -> String {
        "David"
    }
}
```

If we want to test the `DataManager` class in this example, we'd be dependent on the `ApiClient` working as expected - that's not what we want, since we only want to test the behaviour of the `DataManager` class. So let's get to work, and refactor this example.

## Inject that dependency
The first refactoring step we can take, is removing the construction of the dependency from the class. This way, it's up to the caller to provide the dependency to the DataManager, a practice known as [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection). This way, our `DataManager` class will look as follows:

```swift
class DataManager {
    private let client: ApiClient

    public init(client: ApiClient) {
        self.client = client
    }

    public func getUserInfo() -> UserInfo {
        let name = client.getUserName()
        return UserInfo(name: name)
    }
}
```

While that removes the constructing of `ApiClient` from `DataManager`, it does not remove the dependency on this exact implementation - so let's continue our refactoring.

## Introducing a protocol
Our next step is to make `DataManager` unaware of the exact implementation of `ApiClient`, since `DataManager` only has to know that the `getUserName()` function will return a `String`. To do this, we'll introduce a protocol for the `ApiClient` and refactor the `DataManager` to expect an instance of a class implementing that protocol.

```swift
class DataManager {
    private let client: ApiClientProtocol

    public init(client: ApiClientProtocol) {
        self.client = client
    }

    public func getUserInfo() -> UserInfo {
        let name = client.getUserName()
        return UserInfo(name: name)
    }
}

protocol ApiClientProtocol {
    func getUserName() -> String
}

class ApiClient: ApiClientProtocol {
    // Imagine a call to an API begin performed here
    public func getUserName() -> String {
        "David"
    }
}
```

With this in place, we can create multiple different classes implementing the `ApiClientProtocol` and pass those to the `DataManager`, allowing us to replace the actual `ApiClient` from our application code with a special test version in our test code.

## Writing a test
With this setup in place, let's look at a simple test case for our `DataManager`. 

```swift
final class DataManagerTests: XCTestCase {
    let apiClient = FakeApiClient()
    lazy var dataManager = DataManager(client: apiClient)

    func testGetUserInfo_ReturnsUserInformation() {
        var result = dataManager.getUserInfo()

        XCTAssertEqual("my-test-name", result.name)
    }
}

class FakeApiClient: ApiClientProtocol {
    func getUserName() -> String {
        "my-test-name"
    }
}
```

By having an instance of a class implementing the `ApiClientProtocol` injected into the `DataManager`, we can easily create a special class just for tests to fake the behaviour of the actual `ApiClient`, and test the behaviour of `DataManager`. 

This is already very nice, but still pretty limited - without a lot of extra code, we can not easily set different return values, have the function throw, or validate if the function has actually been called. And with more and more functions added to the protocol, each and every implementation of the protocol that you write for your tests has to be extended - not really a maintainable solution. Luckily, there are packages that can help us out here.

## Set up the mocking library
Instead of creating our own test version of a class implementing the `ApiClientProtocol`, we'll use a mocking package to do the heavy lifting for us. There are multiple mocking packages for Swift, but I'll be using [MockSwift](https://github.com/leoture/MockSwift) here. Setting it up requires some work, since it has to work around the limitations of Swift. 

To set up MockSwift, make sure you have [Sourcery](https://github.com/krzysztofzablocki/Sourcery) and the package installed. Then, create a script called `gen-mocks.sh` with the following content (updating the paths if required):

```shell
sourcery \
--sources . \
--templates Tests/MockSwift.stencil \
--output Tests/GeneratedMocks.swift \
--args module=<YOUR MODULE>
```

To make sure that our protocol can be mocked, we need to mark it as mockable. We can do so by adding an annotation to our protocol:

```swift
// sourcery: AutoMockable
protocol ApiClientProtocol {
    func getUserName() -> String
}
```

If we now run the `gen-mocks.sh` script, the generated file `GeneratedMocks.swift` will contain all the code we need to create mocks in our tests - so let's do so! 

## Let's mock it

First, we'll remove the `FakeApiClient` from our test file, and import the `MockSwift` package. Then, we'll update the definition of the `apiClient` variable with the `@Mock` annotation, and we'll replace the assignment with a typehint:

```swift
@Mock var apiClient: ApiClientProtocol
```

That's all the code needed to create a mock of our protocol - now we just need to define the behaviour of the mock when a certain function is called. This, we can do as follows:

```swift
given(apiClient).getUserName().willReturn("my-test-name")
```

This will make our mock behave in the same way as our old `FakeApiClient` class did - but with a lot of advantages. We can now also validate that the `DataManager` indeed called the function as expected:

```swift
then(apiClient).getUserName().called(times: 1)
```

Working with this mocking library also allows different return values of the function, or even having the function throw, all to reliably test the behaviour of the class you're actually testing. Our test case finally looks like this:

```swift
import XCTest
import MockSwift

final class DataManagerTests: XCTestCase {
    @Mock var apiClient: ApiClientProtocol
    lazy var dataManager = DataManager(client: apiClient)

    func testGetUserInfo_ReturnsUserInformation() {
        given(apiClient).getUserName().willReturn("my-test-name")

        var result = dataManager.getUserInfo()

        then(apiClient).getUserName().called(times: 1)
        XCTAssertEqual("my-test-name", result.name)
    }
}
```

Using mocks in your tests allows you to reliably unit test your classes, without worrying about its dependencies. In Swift, it's quite a bit of work to set it up - but when you've done so, writing tests become a breeze. 

Happy testing!