OCMock stubbing with String return type
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?
@TadeasKriz might know, he's the brain behind integrating OCMock into Cuckoo.
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
UserDefaultsorFileManagerin 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 doclass FileManager: Foundation.FileManagerif you can stand the namespacing thing or you can try to come up with a different naming convention
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())
}
}
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'