erikdoe / ocmock

Mock objects for Objective-C
http://ocmock.org
Apache License 2.0
2.16k stars 606 forks source link

OCMArg invokeWithArgs doesn't use provided arguments for the block #293

Closed GeroHerkenrath closed 8 years ago

GeroHerkenrath commented 8 years ago

Okay, I'm still a bit new to OCMock but it served me well so far. Unfortunately I'm running into an issue with testing a block's functionality, this is my test:

it(@"it has correct states afterwards when the checkout went wrong", ^{
    id coVCmock = OCMClassMock([TRSCheckoutViewController class]);
    __block TRSOrder *helperPointer = aTestOrder; // initialized in a beforeEach block
    __block NSError *anError = [NSError errorWithDomain:@"nomatter" code:9 userInfo:nil];
    __block NSNumber *wrappedBool = [NSNumber numberWithBool:NO];
    OCMStub([coVCmock processOrder:[OCMArg any] onCompletion:([OCMArg invokeBlockWithArgs:wrappedBool, anError, nil])]);
    void (^aCallback)(NSError * _Nullable error) = ^void(NSError * _Nullable error) {
        expect(error).toNot.beNil();
        expect(helperPointer.insuranceState).to.equal(TRSInsured);
    };
    [helperPointer finishWithCompletionBlock:aCallback];
});

Basically the finishWithCompletionBlock: method does most of its work in another block that is passed to an object of my TRSCheckoutViewController class. That's also where aCallback is called from. The stub works fine, i.e. instead of actually calling the method on TRSCheckoutViewController it does invoke the block, but the parameters are always YES and nil, which I assume are the defaults that are also used when just using [OCMArg invokeBlock].

Is this a bug or did I miss anything? Maybe it has to do with the fact that TRSCheckoutViewController is, as the name implies, a viewcontroller, but I don't actually do anything with it, so I don't get where the problem lies.

SteveFortune commented 8 years ago

Thanks for the feedback. I haven't looked at OCMock in a while, but I think the issue is that you're stubbing the instance method -(void)processOrder:onCompletion: as if it were a class method.

Because coVCmock is never passed to aTestOrder, [helperPointer finishWithCompletionBlock:aCallback] calls the method on its own TRSCheckoutViewController instance (checkoutVC), rather than on your mock.

I cloned your repo and added some logs to confirm the issue; see this commit. Executing the test on my machine yields:

...
Test Case '-[TRSOrderSpec test_TRSOrder__validation_and_API_calls_with_a_valid_and_new_order__finishWithCompletionBlock__after_webView_was_dismissed__it_has_correct_states_afterwards_when_the_checkout_went_wrong]' started.
2016-05-01 19:24:46.423 Trustbadge_Example[38674:1141681] --- Mock controller OCMockObject(TRSCheckoutViewController)
2016-05-01 19:24:46.424 Trustbadge_Example[38674:1141681] --- Checkout controller, <TRSCheckoutViewController: 0x7fec49c51670>
2016-05-01 19:24:46.424 Trustbadge_Example[38674:1141681] --- Controller implementation called, <__NSStackBlock__: 0x7fff52ce5700>
Test Case '-[TRSOrderSpec test_TRSOrder__validation_and_API_calls_with_a_valid_and_new_order__finishWithCompletionBlock__after_webView_was_dismissed__it_has_correct_states_afterwards_when_the_checkout_went_wrong]' passed (0.007 seconds).
...

So from what I can tell, the controller instance used in -(void)finishWithCompletionBlock: is not the mock and the actual -(void)processOrder:onCompletion: implementation is called. To fix this, you could:

I've implemented an example fix in this commit, which yields:

...
2016-05-01 19:54:01.090 Trustbadge_Example[40452:1170987] --- Mock controller OCMockObject(TRSCheckoutViewController)
2016-05-01 19:54:01.090 Trustbadge_Example[40452:1170987] --- Checkout controller, OCMockObject(TRSCheckoutViewController)
2016-05-01 19:54:01.091 Trustbadge_Example[40452:1170987] --- Order processed: 0, Error Domain=nomatter Code=9 "(null)"
Test Case '-[TRSOrderSpec test_TRSOrder__validation_and_API_calls_with_a_valid_and_new_order__finishWithCompletionBlock__after_webView_was_dismissed__it_has_correct_states_afterwards_when_the_checkout_went_wrong]' passed (0.018 seconds).
...

The block is successfully invoked: Order processed: 0, Error Domain=nomatter Code=9 "(null)".

GeroHerkenrath commented 8 years ago

Thanks a lot, this explains everything. It actually explained to me how OCMock stubs methods. Until now I thought it stubs every instance's method, my bad. I am busy with some other stuff for the SDK for now, but will probably include your draft in the next patch. I was hesitant at first to change my implementation just because I can't get a test for it to work, but the idea with a creating method such as new (or another class convenience creator) is a good solution. Since this is not really an issue with OCMock I'll close the issue.