Brightify / Cuckoo

Boilerplate-free mocking framework for Swift!
MIT License
1.67k stars 174 forks source link

Mocking a delegate property with objcStub #358

Open s-hocking opened 4 years ago

s-hocking commented 4 years ago

Hi there. I'm currently trying to mock out CBCentralManager using the experimental ObjC stubbing feature. I've run into an issue with the delegate property on this class... If I don't stub the delegate property, OCMock fails my test when my code accesses the delegate property, with "unexpected method invoked":

caught "NSInternalInconsistencyException", "OCMockObject(CBCentralManager): unexpected method invoked: setDelegate:<MySDKProject.BLECentral: 0x7fa1964178a0>

So I then try to stub the delegate property, but nothing seems to work.

What am I doing wrong?

Edit: for anyone else trying to mock CoreBluetooth classes, this project from Nordic Semiconductor looks pretty good!

MatyasKriz commented 4 years ago

I'm afraid you're not using the correct methods for ObjC mocking. Please post the whole test, so I can verify how you create mocks, stub them, and verify them.

s-hocking commented 4 years ago

Thanks for the response. Here's a brief test class that illustrates my problem:

class CuckooBluetoothDemo: XCTestCase {

    var mockCentral: CBCentralManager!

    override func setUpWithError() throws {
        mockCentral = objcStub(for: CBCentralManager.self) { stubber, mock in
            // Neither of these stubs compile:
            stubber.when(mock.delegate.set).then { args in // Value of type 'CBCentralManagerDelegate?' has no member 'set'

            }

            stubber.when(mock.setDelegate).then { args in // Value of type 'CBCentralManager' has no member 'setDelegate'

            }
        }
    }

    func testSettingDelegate() throws {
        // This line triggers an error because the delegate is not stubbed:
        // caught "NSInternalInconsistencyException", "OCMockObject(CBCentralManager): unexpected method invoked: setDelegate:-[CuckooBluetoothDemo testSettingDelegate] "
        mockCentral.delegate = self
    }
}

extension CuckooBluetoothDemo: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        // This method is just here to satisfy the compiler
    }
}
MatyasKriz commented 4 years ago

Hey, I just played around a bit with this. We were missing a NSObjectProtocol support.

Please try branch fix/objc-nsobjectprotocol, no need to rebuild/redownload the generator, this change affects only Cuckoo internals, so just install pods again after changing the Podfile.

Thank you so much for bringing this to light, if you come up with any other shortcomings, let us know and we'll hopefully find a way to fix them.

Oh and in ObjC mocking, you needn't specify anything like .set, just call it like you would, it's an autoclosure to make sure that OCMock can do its thing.

s-hocking commented 4 years ago

Thanks for looking in to this! I've tried out your bug fix branch, which makes it possible to mock out the delegate return value 👍

I've still got an issue though when the delegate property is set. The test fails because setDelegate: is not mocked. How can I mock this method?

class CuckooBluetoothDemo: XCTestCase {

    var mockCentral: CBCentralManager!

    override func setUpWithError() throws {
        mockCentral = objcStub(for: CBCentralManager.self) { stubber, mock in
            stubber.when(mock.delegate).thenReturn(self) // this now works with the updated branch!
        }
    }

    func testSettingDelegate() throws {
        // This line still fails the test because setDelegate: is not stubbed, with error:
        // caught "NSInternalInconsistencyException", "OCMockObject(CBCentralManager): unexpected method invoked: setDelegate:-[CuckooBluetoothDemo testSettingDelegate] "
        mockCentral.delegate = self
    }
}

extension CuckooBluetoothDemo: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        // This method is just here to satisfy the compiler
    }
}
MatyasKriz commented 4 years ago

Thanks for the feedback, I'll take a look at it, hopefully it's nothing too major.

quetool commented 4 years ago

Hi! I just ran into this myself too, how do I need to change Podfile to add fix/objc-nsobjectprotocol branch?

quetool commented 4 years ago

Like this?

pod 'Cuckoo', :git => 'https://github.com/Brightify/Cuckoo.git', :branch => 'fix/objc-nsobjectprotocol'
quetool commented 4 years ago

I've changed the Podfile, still getting the same error but I am not sure it is related with the poster´s issue

Captura de Pantalla 2020-08-03 a la(s) 19 11 55

caught "NSInternalInconsistencyException", "OCMockObject(MiBancoAPIClient): unexpected method invoked: expireSessionWithCompletion:<__NSMallocBlock__: 0x600000054240> 
    stubbed:    hostAvailable
    stubbed:    isSessionStillValid"
(
    0   CoreFoundation                      0x00007fff23e3cf0e __exceptionPreprocess + 350
    1   libobjc.A.dylib                     0x00007fff50ba89b2 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff23e3cd4c +[NSException raise:format:] + 188
    3   OCMock                              0x0000000109ab76ce -[OCMockObject handleUnRecordedInvocation:] + 190
    4   OCMock                              0x0000000109ab6a2d -[OCMockObject forwardInvocation:] + 93
    5   CoreFoundation                      0x00007fff23e416b6 ___forwarding___ + 838
    6   CoreFoundation                      0x00007fff23e43bf8 _CF_forwarding_prep_0 + 120
    7   CoreFoundation                      0x00007fff23e43e8c __invoking___ + 140
    8   CoreFoundation                      0x00007fff23e41071 -[NSInvocation invoke] + 321
    9   CoreFoundation                      0x00007fff23e41344 -[NSInvocation invokeWithTarget:] + 68
    10  Cuckoo                              0x00000001099e191a -[CuckooMockObject forwardInvocation:] + 586
    11  CoreFoundation                      0x00007fff23e416b6 ___forwarding___ + 838
    12  CoreFoundation                      0x00007fff23e43bf8 _CF_forwarding_prep_0 + 120
    13  MiBancoSwiftTests                   0x00000001093affa6 $s17MiBancoSwiftTests06CuckooD0C7testApiyyF + 246
    14  MiBancoSwiftTests                   0x00000001093b035b $s17MiBancoSwiftTests06CuckooD0C7testApiyyFTo + 43
    15  CoreFoundation                      0x00007fff23e43e8c __invoking___ + 140
    16  CoreFoundation                      0x00007fff23e41071 -[NSInvocation invoke] + 321
    17  XCTest                              0x0000000109082037 __24-[XCTestCase invokeTest]_block_invoke_2 + 52
    18  XCTest                              0x0000000109081fe3 __24-[XCTestCase invokeTest]_block_invoke.206 + 320
    19  XCTest                              0x00000001090dcdc2 +[XCTestCase(Failures) performFailableBlock:testCase:testCaseRun:shouldInterruptTest:] + 69
    20  XCTest                              0x00000001090dccd4 -[XCTestCase(Failures) _performTurningExceptionsIntoFailuresInterruptAfterHandling:block:] + 115
    21  XCTest                              0x00000001090819f6 -[XCTestCase invokeTest] + 1183
    22  XCTest                              0x0000000109083329 __26-[XCTestCase performTest:]_block_invoke_2 + 43
    23  XCTest                              0x00000001090dcdc2 +[XCTestCase(Failures) performFailableBlock:testCase:testCaseRun:shouldInterruptTest:] + 69
    24  XCTest                              0x00000001090dccd4 -[XCTestCase(Failures) _performTurningExceptionsIntoFailuresInterruptAfterHandling:block:] + 115
    25  XCTest                              0x0000000109083260 __26-[XCTestCase performTest:]_block_invoke.359 + 86
    26  XCTest                              0x00000001090efa0d +[XCTContext runInContextForTestCase:block:] + 211
    27  XCTest                              0x0000000109082b14 -[XCTestCase performTest:] + 566
    28  XCTest                              0x00000001090c938e -[XCTest runTest] + 57
    29  XCTest                              0x000000010907cd50 __27-[XCTestSuite performTest:]_block_invoke + 354
    30  XCTest                              0x000000010907c4a2 __59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
    31  XCTest                              0x00000001090efa0d +[XCTContext runInContextForTestCase:block:] + 211
    32  XCTest                              0x000000010907c459 -[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
    33  XCTest                              0x000000010907c7be -[XCTestSuite performTest:] + 348
    34  XCTest                              0x00000001090c938e -[XCTest runTest] + 57
    35  XCTest                              0x000000010907cd50 __27-[XCTestSuite performTest:]_block_invoke + 354
    36  XCTest                              0x000000010907c4a2 __59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
    37  XCTest                              0x00000001090efa0d +[XCTContext runInContextForTestCase:block:] + 211
    38  XCTest                              0x000000010907c459 -[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
    39  XCTest                              0x000000010907c7be -[XCTestSuite performTest:] + 348
    40  XCTest                              0x00000001090c938e -[XCTest runTest] + 57
    41  XCTest                              0x000000010907cd50 __27-[XCTestSuite performTest:]_block_invoke + 354
    42  XCTest                              0x000000010907c4a2 __59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
    43  XCTest                              0x00000001090efa0d +[XCTContext runInContextForTestCase:block:] + 211
    44  XCTest                              0x000000010907c459 -[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
    45  XCTest                              0x000000010907c7be -[XCTestSuite performTest:] + 348
    46  XCTest                              0x00000001090c938e -[XCTest runTest] + 57
    47  XCTest                              0x00000001090fef14 __44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke + 171
    48  XCTest                              0x00000001090ff001 __44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke.100 + 96
    49  XCTest                              0x0000000109097746 -[XCTestObservationCenter _observeTestExecutionForBlock:] + 682
    50  XCTest                              0x00000001090fec9f -[XCTTestRunSession runTestsAndReturnError:] + 615
    51  XCTest                              0x0000000109060744 -[XCTestDriver runTestsAndReturnError:] + 456
    52  XCTest                              0x00000001090eb64c _XCTestMain + 2496
    53  libXCTestBundleInject.dylib         0x0000000107c4cbfa __copy_helper_block_e8_32s + 0
    54  CoreFoundation                      0x00007fff23da0b5c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    55  CoreFoundation                      0x00007fff23da0253 __CFRunLoopDoBlocks + 195
    56  CoreFoundation                      0x00007fff23d9b043 __CFRunLoopRun + 995
    57  CoreFoundation                      0x00007fff23d9a944 CFRunLoopRunSpecific + 404
    58  GraphicsServices                    0x00007fff38ba6c1a GSEventRunModal + 139
    59  UIKitCore                           0x00007fff48c8b9ec UIApplicationMain + 1605
    60  MiBanco Dev                         0x0000000106926bc0 main + 112
    61  libdyld.dylib                       0x00007fff51a231fd start + 1
    62  ???                                 0x0000000000000005 0x0 + 5
)
MatyasKriz commented 4 years ago

Hey, @quetool. Your issue is not quite relevant to @s-hocking's. Your mockApiClient doesn't stub the expireSession method, so OCMock tells you that in the error message.

As for calling the setter for resolving @s-hocking, I'm pretty stumped without the necessary experience in ObjC to include this functionality at the moment. I'll leave the issue open and see if either I or @TadeasKriz gets some bright idea.

jandrewmoore commented 2 years ago

I am having almost the same issue as @s-hocking. Using objcStub(), the class I'm testing sets delegate on the mock, test fails.

Looking through the OCMock code and documentation, it seems like maybe the mock that objcStub() creates is a strict mock and will always fail if an unstubbed method is invoked (setDelegate in this case). If Cuckoo/OCMock exposes a way to make "nice" mocks, @s-hocking and I might be unstuck.

OCMock's method for making "nice" mocks: https://github.com/erikdoe/ocmock/blob/6358799e04cb93d8f126f1ea6a67e2e351b169f1/Examples/iOS5Example/usr/include/OCMock/OCMockObject.h#L22

MatyasKriz commented 2 years ago

Hey @jandrewmoore, that might work, and even if it doesn't for some reason, it's still a good feature to have. Thanks for doing the research. 🙂