Cuckoo icon indicating copy to clipboard operation
Cuckoo copied to clipboard

OCMock stubbing with String return type

Open Tyler-Keith-Thompson opened this issue 4 years ago • 4 comments

So I was trying to mock UserDefaults string(forKey:) method using OCMock. My attempt:

        let mock = objcStub(for: UserDefaults.self) { (stubber, mock) in
            stubber.when(mock.string(forKey: "key")).thenReturn("value") //SEG FAULT 11
        }

This blows up spectacularly with a segfault. After digging into a bit I have a couple of theories. It seems to me this is some issue with NSString vs String types. The method signature for UserDefaults (despite being an objective-c type) as exposed to Swift returns a String?. It seems probable that it honestly returns an NSString and there is some language construct that just converts that.

Note that other UserDefaults kinds of things work as expected (bool, for example).

Any advice for how to get around this?

Tyler-Keith-Thompson avatar Jan 15 '21 17:01 Tyler-Keith-Thompson

@TadeasKriz might know, he's the brain behind integrating OCMock into Cuckoo.

MatyasKriz avatar Feb 02 '21 17:02 MatyasKriz

Update on this: Turns out this is really a problem with anything that's a swift value type that the language sort of auto-converts from Objective-C. For example when mocking FileManager we ran into this same problem because contents(atPath:) returns a Data? struct instead of an NSData object. So fundamentally it can't handle a thenReturn method.

Seems there are a couple workarounds:

  • Create a protocol in your production code and mock that 🤮. Unfortunately this means changing production code purely for the sake of tests
  • Create a subclass of UserDefaults or FileManager in your test target only, point Cuckoo at that file in your test target. Downside: You have to override any method you want it to mock, also naming things becomes hard. You could do class FileManager: Foundation.FileManager if you can stand the namespacing thing or you can try to come up with a different naming convention

Tyler-Keith-Thompson avatar Feb 02 '21 19:02 Tyler-Keith-Thompson

I managed to make this work by utilizing _ObjectiveCBridgeable protocol by adding extensions to Stubber and StubRecorder. (see below)

By adding this to my project I managed to make it work for String along with other bridgeable types.

import Cuckoo

extension Stubber {
    func when<OUT: _ObjectiveCBridgeable>(_ invocation: @autoclosure () -> OUT) -> StubRecorder<OUT._ObjectiveCType> {
        when(invocation()._bridgeToObjectiveC())
    }

    func when<OUT: _ObjectiveCBridgeable>(_ invocation: @autoclosure () -> OUT?) -> StubRecorder<OUT._ObjectiveCType?> {
        when(invocation()?._bridgeToObjectiveC())
    }
}

extension StubRecorder {
    func thenReturn<T: _ObjectiveCBridgeable>(
        _ value: T
    ) where T._ObjectiveCType == OUT, T._ObjectiveCType: NSObject {
        thenReturn(value._bridgeToObjectiveC())
    }

    func thenReturn<T: _ObjectiveCBridgeable>(
        _ value: T?
    ) where T._ObjectiveCType? == OUT, T._ObjectiveCType: NSObject {
        thenReturn(value?._bridgeToObjectiveC())
    }
}

rolandkakonyi avatar Mar 30 '22 12:03 rolandkakonyi

The solution works perfectly for bridgeable classes, such as String.

But how do we create stubbing with Enumeration type?

    func testObjectiveC() {
        let mock = objcStub(for: UIDevice.self) { stubber, mock in
            stubber.when(mock.batteryState).thenReturn(UIDevice.BatteryState.charging)
        }
        XCTAssertEqual(mock.batteryState, .charging)
    }

Can't be built now. The compiler reports Instance method 'thenReturn' requires that 'UIDevice.BatteryState' conform to '_ObjectiveCBridgeable'

drwjf avatar Apr 14 '23 17:04 drwjf