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.