Published on Apr 3, 2021
Tagged with:Mirror for Tests
When we started creating tests we learned to test the behavior of a class using its public methods. We understand that private code should not have a side effect on the behavior of a class and therefore does not need to be tested. These statements are correct, but are there no contexts in which private code should be tested? And how can we test private code without hurting our architecture?
We will discuss in this article about the Mirror API and what its use cases are in testing with private properties.
What's Mirror ?
Reflection is a common feature of programming languages โโthat allows you to perform introspection and manipulation of a type at runtime. Swift provides reflection through the Mirror API that represents an instance of any type and describes the parts that comprise it.
Introspection is the ability to inspect an object's type and properties at runtime.
Mirror API
Mirror's API is partially implemented in Swift and C ++ with a C layer connecting the two ends. This implementation allows the Mirror API to be flexible and dynamic, even though Swift is a static language. The mirror uses the runtime of C ++ and Objective-C to perform type introspection. Type introspection needs to be dynamically sent because of some specificities, such as the need to search for children properties in different ways depending on the type being reflected, and the different treatment between the Swift and Objective-C classes. This dynamic sending is similar to the dynamic dispatch of the Method Dispatch, which I already explained in another article that can be accessed here.
Simplified representation of the Mirror API in Swift
public struct Mirror {
public init(reflection subject: Any)
public typealias Child = (label: String?, value: Any)
public typealias Children = AnyCollection<Mirror.Child>
public enum DisplayStyle {
case `struct`
case `class`
case `enum`
case tuple
case optional
case collection
case dictionary
case `set`
}
}
Mirror(reflection:)
: Init that accepts to receive any value.
Child
: It's a tuple that contains a label and a value that represents a property of the reflected instance. The label is the name of the property and the value is its type.
Children
: A collection of any type that contains the children of the reflected instance.
DisplayStyle
: A suggested display style for the reflected type.
How to use Mirror
Mirror is very easy to use when you learn how it works, we will see in the following example how to use it.
//1 - Create object
struct ExampleStruct {
private let storedProperty = "Stored property example"
}
final class ExampleObject {
private let storedProperty = ExampleStruct()
private var computedProperty: String {
return "Computed property example"
}
}
//2 - Instantiate Object
let exampleObject = ExampleObject()
//3 - Create Mirror from object instance
let exampleObjectMirrored = Mirror(reflecting: exampleObject.self) //Output: Mirror for ExampleObject
//4 - Children iteration
exampleObjectMirrored.children.forEach { child in
print("Label:\(child.label)")
print("Value:\(child.value)")
}
/*
* Output:
* Label: storedProperty
* Value: ExampleStruct(storedProperty: "Stored property example")
*/
//5 - Filter especific child
let exampleStruct = exampleObjectMirrored.children.first { $0.label == "storedProperty" }?.value as? ExampleStruct()
//Output: Optional(ExampleStruct(storedProperty: "Stored property example"))
let exampleStructMirrored = Mirror(reflecting: exampleStruct ?? ExampleStruct())
//Output: Mirror for ExampleStruct
let storedProperty = exampleStructMirrored.children.first { $0.label == "storedProperty" }?.value as? String()
//Output: Optional(Stored property example)
Explanation:
- 1 - Create an object
- 2 - Instantiate an object
- 3 - Create mirror from instance of a type: When we create a reflection of an instance, we have access to a structure that represents that type in reading mode. We cannot change the behavior and values โโthat the type has, we can only inspect it.
- 4 - Perform interaction on the children collection: The interaction in children is performed without knowing any of the types at compile time. During the interaction the Mirror will only return the children that are stored properties, the properties computed as in our example will be ignored.
- 5 - Filter a specific child by its label and value: A child has a label and a value and we can use them to do a specific search. We can search for both but if there are properties with the same type it is better to search using the label.
Custom Reflectable
Configurator is responsible for creating a scene by instantiate all the objects that make up the scene and passing the reference that each one needs. The connections between the objects are made through protocols and some need to have a weak reference in order not to generate retain cicle. And if the configurator is creating the wrong instance of an object and not defining the reference between some components of the cycle causing a bug in the application. How can we ensure that this scenario will not happen?
We can test this scenario using the Mirror to search the references that each object has from the other and make the statements verifying if the references were passed and if they are of the expected type. See the test below:
extension ExampleObject: CustomReflectable {
var customMirror: Mirror {
return Mirror(
self,
children: [
"storedProperty": storedProperty,
"computedProperty": computedProperty
]
)
}
}
exampleObjectMirrored.children.forEach { child in
print("Label:\(child.label)")
print("Value:\(child.value)")
}
Output:
Label: storedProperty
Value: ExampleStruct(storedProperty: "Stored property example")
Label: computedProperty
Value: Computed property example
Making it easy to use
Mirror is easy to use, but it has verbose syntax which makes reading the code a little difficult. A good practice is to create some methods that encapsulate the logic of searching for children within an extension. There are several ways to implement this abstraction, see it an example below.
extension Mirror {
public func firstChild(of _: T.Type, in label: String? = nil) -> T? {
children.lazy.compactMap {
guard let value = $0.value as? T else { return nil }
guard let label = label else { return value }
return $0.label == label ? value : nil
}.first
}
public func reflectFirstChild(of type: T.Type, in label: String? = nil) -> Mirror? {
guard let child = firstChild(of: T.Type, in: label) else {
return nil
}
return Mirror(reflecting: child)
}
}
Use Case in tests
Now let's go to the main part of this article and learn some use cases for Mirror in the tests.
Factory tests
Factory creates the concrete implementation of a complex object and injects the dependencies it needs. How do we know that these dependencies that are private properties of the class have been injected correctly? To test this scenario, you will use the mirror to access the private dependency that the object contains and check if it was injected correctly. See the test below:
final class FactoryTests: XCTestCase {
private let sut = Factory()
private lazy var repositorySpy = RepositorySpy()
func test_whenBuildUseCase_thenPassedCorrectDependencies() {
let dependenciesDummy = Factory.Dependencies(
repository: repositorySpy
)
let useCaseExpected = sut.buildUseCase(dependencies: dependenciesDummy)
let useCaseMirrored = Mirror(reflecting: useCaseExpected)
let repository = useCaseMirrored.firstChild(of: RepositoryProtocol.self)
XCTAssertTrue(repository === repositorySpy)
}
}
Configurator tests
Configurator is responsible for creating a scene by instantiate all the objects that make up the scene and passing the reference that each one needs. The connections between the objects are made through protocols and some need to have a weak reference in order not to generate retain cicle. And if the configurator is creating the wrong instance of an object and not defining the reference between some components of the cycle causing a bug in the application. How can we ensure that this scenario will not happen?
We can test this scenario using the Mirror to search the references that each object has from the other and make the statements verifying if the references were passed and if they are of the expected type. See the test below:
final class ConfiguratorTests: XCTestCase {
private let sut = Configurator()
func whenConfigureVIPScene_thenConectedAllReferences() {
let viewControllerExpected = sut.configure()
let interactor = getInteractorFromViewController(
viewController: viewControllerExpected
)
let viewControllerFromPresenter = getViewControllerFromPresenter(
viewController: viewControllerExpected
)
let viewControllerFromRouter = getViewControllerFromRouter(
viewController: viewControllerExpected
)
XCTAssertTrue(viewControllerExpected is ExampleViewController)
XCTAssertTrue(interactor is Interactor)
XCTAssertTrue(viewControllerFromPresenter === viewControllerExpected)
XCTAssertTrue(viewControllerFromRouter === viewControllerExpected)
}
}
extension ConfiguratorTests {
private func getViewControllerFromPresenter(viewController: UIViewController) -> ViewControllerProtocol? {
let viewControllerMirrored = Mirror(reflecting: viewController)
return viewControllerMirrored.reflectFirstChild(
of: InteractorProtocol.self
)?.reflectFirstChild(
of: PresenterProtocol.self
)?.firstChild(
of: ViewControllerProtocol.self
)
}
private func getViewControllerFromRouter(viewController: UIViewController) -> ViewControllerProtocol? {
let viewControllerMirrored = Mirror(reflecting: viewController)
return viewControllerMirrored.reflectFirstChild(
of: RouterProtocol.self
)?.firstChild(
of: ViewControllerProtocol.self
)
}
private func getInteractorFromViewController(viewController: UIViewController) -> InteractorProtocol? {
let viewControllerMirrored = Mirror(reflecting: viewController)
return viewControllerMirrored.firstChild(
of: InteractorProtocol.self
)
}
}
Delegate and ViewModel tests
A class responsible for creating an ActionSheet has a delegate that handles the events received from the user, and a ViewModel that has the data that must be presented. The delegate is passed through a method and the ViewModel is injected by the class's constructor. How can we ensure that the delegate was passed correctly and that the received ViewModel contains the expected information?
We can ensure that the delegate has been configured correctly and that the content of the ViewModel is as expected with some tests using Mirror to access these properties. See the test below:
final class ActionSheetTests: XCTestCase {
private let actionSheetDelegateSpy = ActionSheetDelegateSpy()
private let viewModel = ActionSheetViewModel.fixture(
description: "descriptionDummy"
)
private lazy var sut = ActionSheet(viewModel: viewModel)
func whenBuildActionSheet_thenSetCorrectDelegate() {
sut.setDelegate(delegate: actionSheetDelegateSpy)
let sutMirrored = Mirror(reflecting: sut)
let delegate = sutMirrored.firstChild(of: ActionSheetDelegate.self)
XCTAssertTrue(delegate === actionSheetDelegateSpy)
}
func whenBuildActionSheet_thenPresentCorrectContent() {
let sutMirrored = Mirror(reflecting: sut)
let descriptionLabel = sutMirrored.reflectFirstChild(
of: ActionSheetViewController.self
)?.reflectFirstChild(
of: ActionSheetView.self
)?.firstChild(of: UILabel.self)
XCTAssertEqual(descriptionLabel?.text, "descriptionDummy")
}
}
Conclusion
The mirror is a powerful tool and it is amazing to know a little about how it performs the magic of reflection. The API allows you to ensure that important scenarios, such as performing dependency injection and instantiate and connecting the components of a scene, have been performed correctly with cheap and quick unit tests.
Thanks for reading! ๐