How to Unit Test Private Methods in Swift

Unit testing a Swift project is quite different from unit testing a project written in Objective-C. For those that are used to the flexibility of the Objective-C runtime, it may feel as if your hands are tied behind your back.

Access Control

While access control is a very welcome addition with many benefits, it can complicate unit testing, especially if you are new to unit testing. You probably know that you can apply the testable attribute to an import statement in a test target to gain access to entities that are declared as internal.

import XCTest

@testable import Notes

class NotesTests: XCTestCase {
    ...
}

While this is a convenient addition, it doesn't give you access to private entities in a test target. This brings us to the question of the day. How do you unit test private entities?

Wrong Question

The short answer to this question is simple. You cannot access private entities from another module and this also applies to test targets. Plain and simple. That is what access control is for.

But that is not the answer to the question. If you ask how to unit test private entities, then you are asking the wrong question. But why is that?

None of Your Business

Why do you declare an entity private? What is your motivation for doing so? Take a look at the following example.

import Foundation

struct AccountViewViewModel {

    // MARK: - Properties

    let account: Account

    // MARK: - Public Interface

    var subscriptionAsString: String {
        switch account.subscription {
        case .monthly: return "Monthly Subscription"
        case .yearly: return "Yearly Subscription"
        case .trial: return "Trial Subscription"
        }
    }

    var expiresAtAsString: String {
        // Parse Date
        let date = parse(date: account.expiresAt)

        // Initialize Date Formatter
        let dateFormatter = DateFormatter()

        // Configure Date Formatter
        dateFormatter.dateFormat = "YYYY, MMMM"

        // Convert Date to String
        return dateFormatter.string(from: date)
    }

    // MARK: - Private Interface

    private func parse(date dateAsString: String) -> Date {
        let dateFormat: String

        if dateAsString.contains("/") {
            dateFormat = "YYYY'/'MM'/'dd"
        } else {
            dateFormat = "YYYYMMdd"
        }

        // Initialize Date Formatter
        let dateFormatter = DateFormatter()

        // Configure Date Formatter
        dateFormatter.dateFormat = dateFormat

        if let date = dateFormatter.date(from: dateAsString) {
            print(date)
            return date
        } else {
            fatalError("Incompatible Date Format")
        }
    }

}

I would like to unit test the AccountViewViewModel structure. As you can see, the AccountViewViewModel struct exposes two internal computed properties and it also defines a private method. The expiresAtAsString computed property offloads some of its work to the private parse(date:) method. Testing the internal computed properties is straightforward.

// MARK: - Tests for Subscription as String

func testSubscriptionAsString_Monthly() {
    let account = Account(expiresAt: "20161225", subscription: .monthly)
    let accountViewViewModel = AccountViewViewModel(account: account)

    XCTAssertEqual(accountViewViewModel.subscriptionAsString, "Monthly Subscription")
}

func testSubscriptionAsString_Yearly() {
    let account = Account(expiresAt: "20161225", subscription: .yearly)
    let accountViewViewModel = AccountViewViewModel(account: account)

    XCTAssertEqual(accountViewViewModel.subscriptionAsString, "Yearly Subscription")
}

func testSubscriptionAsString_Trial() {
    let account = Account(expiresAt: "20161225", subscription: .trial)
    let accountViewViewModel = AccountViewViewModel(account: account)

    XCTAssertEqual(accountViewViewModel.subscriptionAsString, "Trial Subscription")
}

// MARK: - Tests for Expires at as String

func testExpiresAtAsString_20161225() {
    let account = Account(expiresAt: "20161225", subscription: .trial)
    let accountViewViewModel = AccountViewViewModel(account: account)

    XCTAssertEqual(accountViewViewModel.expiresAtAsString, "2016, December")
}

But how do we test the private method? We cannot access the private method from the test target. But why should we unit test the private method? We marked it as private for a reason. Right? And that brings us to the answer to the question we started with. We don't test private methods.

Unit Testing the Public Interface

By unit testing the public interface of the AccountViewViewModel struct we automatically or implicitly unit test the private interface of the struct. You have the task to make sure the public interface is thoroughly tested. This means that you need to make sure every code path of the AccountViewViewModel struct is covered by unit tests. In other words, the suite of unit tests should result in complete code coverage. That includes public, internal, and private entities.

If we enable code coverage in Xcode and we run the unit tests of the AccountViewViewModel struct, we can see that some code paths are not executed.

Code Coverage in Xcode

This tells us that the unit tests are incomplete. We can ignore the code path for the fatal error. I never unit test code paths that result in a fatal error, but that largely depends on how you use fatal errors in your projects.

We can increase code coverage for the AccountViewViewModel struct by adding one more unit test.

func testExpiresAtAsString_20161225WithForwardSlashes() {
    let account = Account(expiresAt: "2016/12/25", subscription: .trial)
    let accountViewViewModel = AccountViewViewModel(account: account)

    XCTAssertEqual(accountViewViewModel.expiresAtAsString, "2016, December")
}

Code Coverage in Xcode

Implementation and Specification

It is important to understand that we are testing the specification of the AccountViewViewModel struct. We are not testing its implementation. While this may sound similar, it is actually very different. We are testing the functionality of the AccountViewViewModel struct. We are not interested in how it does its magic under the hood.

The key takeaway of this article is that private entities don't need to be unit tested. Unit testing is a form of black-box testing. This means that we don't test the implementation of the AccountViewViewModel struct, we test its specification.

This doesn't mean that we are not interested in the implementation, though. We need to make sure the suite of unit tests covers every code path of the entity we are testing. Code coverage reports are invaluable to accomplish this.