How to Use Mirror Objects in Swift for Testing and Debugging

How to Use Mirror Objects in Swift for Testing and Debugging

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.

What are mirror objects?

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.

Code examples

Let’s see some code examples of how to create and use mirror objects in Swift.

Creating a mirror object

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)

Using a mirror object

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

Practical use cases of mirror objects

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!