How to Use Mirror Objects in Swift for Testing and Debugging

Experienced iOS and React Native Developer with 10+ Years of Expertise - Available for New Opportunities. Connect Now!
Search for a command to run...

Experienced iOS and React Native Developer with 10+ Years of Expertise - Available for New Opportunities. Connect Now!
[[..Pingback..]] This article was curated as a part of #79th Issue of Software Testing Notes Newsletter. Web: https://softwaretestingnotes.com
This time, I'm exploring the fascinating world of AI, stepping away from my usual focus on iOS development. There's so much more to share about AI, and I can't wait to dive into it with you! If you'd like to follow along and receive regular updates, ...

The app is Time Tango, a Pomodoro timer app. I chose this concept after conducting keyword research for popular terms that face minimal competition on the app store. The app is free to use and received approval in its first review. The strategy focus...

In my last article, I talked about how data flows in SwiftUI. Now, Apple has released something new in iOS 17 - the Observation Framework. This changes how SwiftUI works with data. In this article, we will look at how to use the new Observable Macro ...

One of the critical elements of SwiftUI is its approach to managing data flow within applications. This comprehensive guide will provide you with a strong understanding of data flow in SwiftUI, how it operates, and why it's vital for your SwiftUI app...

Arrays are one of the most common data structures used in programming. In Swift, arrays are used to store ordered lists of values of the same type. They are incredibly versatile and powerful, but using them efficiently is key to writing high-performi...

One of Swift's powerful feature features is reflection, which allows us to inspect and manipulate the properties and behaviors of an object at runtime.
Reflection can be useful for testing and debugging purposes, as it can help you verify the state and functionality of your code without relying on external tools or frameworks. In this blog post, we will explore how to use mirror objects in Swift to access type metadata and perform reflection operations.
Mirror objects are instances of the Mirror struct that encapsulate type metadata in Swift. Type metadata is information about the structure, properties, methods, inheritance and protocols of a type. You can create a mirror object for any instance of any type using the Mirror(reflecting:) initializer.
Mirror objects provide read-only access to a subset of type metadata, such as:
The display style of the type (e.g., struct, enum, class)
The subject type of the instance (e.g., Person)
The children of the instance (e.g., name, age)
The superclass of the instance (if any)
The custom mirror of the instance (if any)
The custom leaf reflectable of the instance (if any)
Using mirror objects, you can inspect these aspects of an instance at runtime and use them for testing and debugging purposes.
Let’s see some code examples of how to create and use mirror objects in Swift.
To create a mirror object for an instance, you simply need to call the Mirror(reflecting:) initializer with the instance as an argument. For example:
struct Person {
let name: String
let age: Int
}
let alice = Person(name: "Alice", age: 25)
// Create a mirror object for alice
let mirror = Mirror(reflecting: alice)
Once you have created a mirror object for an instance, you can use its properties and methods to access its type metadata. For example:
// Print some information about alice using the mirror object
print(mirror.displayStyle) // Optional(struct)
print(mirror.subjectType) // Person
print(mirror.children.count) // 2
// Iterate over the children of the mirror object
for child in mirror.children {
print(child.label ?? "", child.value)
}
// name Alice
// age 25
Now that we have seen how to create and use mirror objects in Swift, let’s look at some practical use cases :
Testing and debugging private variables: You can use mirror objects to access and verify the values of private properties that are otherwise inaccessible from outside the type. For example, you can create a helper class that uses mirror objects to extract a property by name and type, and use it in your unit tests or debug console.
// We need to access private properties of this class.
class ViewControllerToTest: UIViewController {
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var headerImageView: UIImageView!
private var secretAgentNames: [String]?
}
// MirrorObject class will accept Any as input and
// provides a series of convenience methods for retrieving
// a particular property by name.
class MirrorObject {
let mirror: Mirror
init(reflecting: Any) {
mirror = Mirror(reflecting: reflecting)
}
func extract<T>(variableName: StaticString = #function) -> T? {
extract(variableName: variableName, mirror: mirror)
}
private func extract<T>(variableName: StaticString, mirror: Mirror?) -> T? {
guard let mirror = mirror else {
return nil
}
guard let descendant = mirror.descendant("\(variableName)") as? T else {
return extract(variableName: variableName, mirror: mirror.superclassMirror)
}
return descendant
}
}
Now, in our testing target:
final class ViewControllerToTestMirror: MirrorObject {
init(viewController: UIViewController) {
super.init(reflecting: viewController)
}
// List all private properties you wish to test using SAME NAME.
var headerImageView: UIImageView? {
extract()
}
var titleLabel: UILabel? {
extract()
}
var secretAgentNames: [String]? {
extract()
}
}
// Create a mirror object
let viewControllerMirror = ViewControllerToTestMirror(viewController: ViewControllerToTest())
// Access the private properties in your unit tests
viewControllerMirror.headerImageView
viewControllerMirror.titleLabel
viewControllerMirror.secretAgentNames
Grouping errors by enum cases: You can use mirror objects to get the names of enum cases at runtime, and use them to group errors by their source or category. For example, you can create a custom error type that conforms to CustomStringConvertible and uses mirror objects to return a descriptive string based on its case name.
enum ArticleState {
case scheduled(Date)
case published
}
let scheduledState = ArticleState.scheduled(Date())
Mirror(reflecting: scheduledState).children.forEach { child in
print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}
// Prints:
// Found child 'scheduled' with value '2021-12-21 08:16:48 +0000'
Preloading data from a database: You can use mirror objects to iterate over the properties of an object and preload them with data from a database. For example, you can create an extension for your model types that uses mirror objects to reset all their properties with default values or cached data.
extension UserSession {
func logOut() {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if let resettable = child.value as? Resettable {
resettable.reset()
}
}
}
}
I hope this blog post has given you some insights into how to use mirror objects in Swift for testing and debugging purposes. If you have any questions or feedback, please leave a comment below. Thank you for reading!