Creating false instances of a type for the sake of side-effects (or: Please tell me why this is a Bad Idea)

OK so hear me out.

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
}

Bump? :(

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
2 Likes

The docs for unsafeBitCast(_:to:) state that using it with reference types is undefined behavior.

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...

1 Like

@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.

1 Like

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.

1 Like