Imagine there's a type that I don't have access to the initializer for, or for other reasons I can't create. I have a property that accepts and returns this type, and I want to test that this exact property is called when I expect.
var someComputedProperty: SomeTypeIDontOwn {
get {
fatalError(":)")
}
set {
print("Setter called!")
}
}
Now, also imagine that:
I can't solve this problem in any of the obvious, safer ways β protocols, shims, etc.
I always control the body of set
and that I decide to create a false instance of SomeTypeIDontOwn, purely for the sake of triggering set.
What's the worse that could happen with the following?
class Farce { }
func withUnsafeFalseInstance<T>(of: T.Type, body: (T) -> Void) {
let isReferenceType = T.self is AnyObject.Type
if isReferenceType {
let farce = unsafeBitCast(Farce(), to: T.self)
body(farce)
} else {
let size = MemoryLayout<T>.size
let ptr = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
ptr.initialize(repeating: 0, count: size)
ptr.withMemoryRebound(to: T.self, capacity: 1) {
body($0.pointee)
}
ptr.deallocate()
}
}
withUnsafeFalseInstance(of: SomeTypeIDontOwn.self) {
someComputedProperty = $0 // Ideally: "Setter called!" is printed
}
The Earth may implode, or you trigger the zombie apocalypse. It's undefined behaviour, all bets are off.
What I would advise instead is to mock/stub the types you don't own. See this StackOverflow answer if you're unclear about the difference.
So, create a module containing fake versions (mocks or stubs) of the types you don't own. It would have a compatible interface, but the functionality would be the bare minimum needed for the module you're really testing. Then, use conditional compilation to substitute the mock/stub module for the real one, and run the tests against that.
// In LibFooStubs
public struct SomeTypeIDontOwn {
public init(...)
}
// In the implementation of the module you're testing
#if USE_LIBFOO_STUBS
import LibFooStubs
#else
import LibFoo
#endif
// Now, the test is able to construct instances of SomeTypeIDontOwn
let stub = SomeTypeIDontOwn()
thing.someComputedProperty = stub
The docs for withMemoryRebound(to:capacity:) state that the types need to be layout compatible. I still donβt understand exactly what counts as layout compatible, but you might find SE-0333 enlightening.
So my guess is that this is invoking undefined behavior in at least one way, possibly in multiple ways. Whatβs the worst that could happen? Probably the usual: crash, memory corruption, memory leak, miscompile, nasal demons...
@Karl@hisekaldma Thank you both for the input! Consensus seems to be that this is uncharted territory and that there isn't probably a predictable/semi-safe way to do this.
You may be able to swizzle the getter via Objc. In the swizzled code you trigger an event validating your test and then return the result of the previous implementation.
I'm not sure if this is feasible but IIRC one can swizzle a property.