Using Mirror to Test Private Properties

There may be times when we’d like to evaluate the state of private properties in unit tests. But we’d rather avoid exposing them publicly as this could make the interface difficult to understand. One way to get around this using reflection via Swift’s Mirror.

Consider a UIViewController with a set of IBOutlets:

class BoardGameViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var numberOfPlayersLabel: UILabel!
    @IBOutlet weak var purchaseButton: UIButton!

    init(boardGame: BoardGame) {
        self.boardGame = boardGame
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        nameLabel.text = boardGame.name
        numberOfPlayersLabel.text = "Up to \(boardGame.numberOfPlayers) players."

        // Hide purchase button if the `BoardGame` is not available.
        purchaseButton.isHidden = !boardGame.availableForPurchase
    }
}

This looks typical. A concern here is that any other class that uses BoardGameViewController would be able to access and modify view properties like nameLabel:

let viewController = BoardGameViewController(boardGame: chess)

// This breaks encapsulation.
viewController.nameLabel.text = "Another name"

We probably don’t want this. We want BoardGameViewController to be in full control of its UI elements. So, we can make the view properties private:

class BoardGameViewController: UIViewController {
    // private properties
    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var numberOfPlayersLabel: UILabel!
    @IBOutlet private weak var purchaseButton: UIButton!
}

This looks great. Other classes would not be able to modify nameLabel and the other properties anymore.

The next challenge is that, in unit tests, we would no longer be able to access the view properties to validate that BoardGameViewController works, and continues to work, as we expect it to.

Let’s say we want to validate that the purchaseButton gets hidden if the BoardGame is not available for purchase. This would be a challenge since purchaseButton is inaccessible. This unit test will not compile:

func testExample() {
    let monopoly = BoardGame(name: "Monopoly",
                             numberOfPlayers: 1_532,
                             availableForPurchase: false)

    let viewController = BoardGameViewController(boardGame: monopoly)
    viewController.loadViewIfNeeded()

    // This doesn't work because `purchaseButton` is inaccessible.
    XCTAssertTrue(viewController.purchaseButton.isHidden)
}

We can solve this by using Mirror to grab the purchaseButton.

// Grab the private `purchaseButton` using Mirror.
let mirror = Mirror(reflecting: viewController)
let purchaseButton = mirror.descendant("purchaseButton") as! UIButton

// We can now test the `purchaseButton` normally.
XCTAssertTrue(purchaseButton.isHidden)

And that does it! We didn’t have to expose purchaseButton publicly but we would still be able to validate that it’s working as we intended it to in unit tests.

You can download an example project of this here: https://github.com/ifcaselet/examples/tree/main/Mirroring.