0

I can not compile this code:

[verify(mockedContext) deleteObject:item1];
[verify(mockedContext) deleteObject:item2];
[verify(mockedContext) save:anything()];<--compilation error for conversion id to NSError**

However I'm able to pass compilation in similar case with given macros with additional syntax:

    [[given([mockedContext save:nil]) withMatcher:anything()] willReturn:nil];

Are there anything to help me pass compilation with verify?

Here is compilation error:

Implicit conversion of an Objective-C pointer to 'NSError *__autoreleasing *' is disallowed with ARC
Eugen Martynov
  • 19,888
  • 10
  • 61
  • 114

1 Answers1

2

I assume the save: method on the 'mockedContext' takes a pointer-to-pointer to NSError.

So actually, the NSError must be seen as an extra return value of the save:method. This means that you should rather setup an expectation in the first place.

I worked out a small example:

We start with the Context protocol with a simple method taking an NSError**.

@protocol Context <NSObject>
- (id)doWithError:(NSError *__autoreleasing *)err;
@end

Next is a class using this protocol, much like your SUT. I called it ContextUsingClass

@interface ContextUsingClass : NSObject

@property (nonatomic, strong) id<Context> context;
@property BOOL recordedError;

- (void)call;

@end

@implementation ContextUsingClass

- (void)call {
    NSError *error;
    [self.context doWithError:&error];
    if (error) {
        self.recordedError = YES;
    }
}

@end

As you can see, when the context method doWithError: returns an error, the recordedError property is set to YES. This is something we can expect to be true or false in our test. The only problem is, how do we tell the mock to result in an error (or to succeed without error)?

The answer is fairly straight forward, and was almost part of your question: we pass an OCHamcrest matcher to the given statement, which in turn will set the error for us through a block. Bear with me, we'll get there. Let's first write the fitting matcher:

typedef void(^ErrorSettingBlock)(NSError **item);

@interface ErrorSettingBlockMatcher : HCBaseMatcher

@property (nonatomic, strong) ErrorSettingBlock errorSettingBlock;

@end


@implementation ErrorSettingBlockMatcher

- (BOOL)matches:(id)item {
    if (self.errorSettingBlock) {
        self.errorSettingBlock((NSError * __autoreleasing *)[item pointerValue]);
    }
    return YES;
}

@end

This matcher will call the errorSettingBlock if it has been set, and will always return YES as it accepts all items. The matchers sole purpose is to set the error, when the test asks as much. From OCMockito issue 22 and it's fix, we learn that pointer-to-pointers are wrapped in NSValue objects, so we should unwrap it, and cast it to our well known NSError **

Now finally, here is how the test looks:

@implementation StackOverFlowAnswersTests {
    id<Context> context;
    ContextUsingClass *sut;
    ErrorSettingBlockMatcher *matcher;
}

- (void)setUp {
    [super setUp];
    context = mockProtocol(@protocol(Context));
    sut = [[ContextUsingClass alloc] init];
    sut.context = context;
    matcher = [[ErrorSettingBlockMatcher alloc] init];
}

- (void)testContextResultsInError {
    matcher.errorSettingBlock = ^(NSError **error) {
        *error = [NSError  errorWithDomain:@"dom" code:-100 userInfo:@{}];
    };

    [[given([context doWithError:nil]) withMatcher:matcher] willReturn:nil];
    [sut call];
    assertThatBool(sut.recordedError, is(equalToBool(YES)));
}

- (void)testContextResultsInSuccess {
    [[given([context doWithError:nil]) withMatcher:matcher] willReturn:nil];
    [sut call];
    assertThatBool(sut.recordedError, is(equalToBool(NO)));
}

@end

Conclusion

When you call methods within your SUT which are returning errors through pointer-to-pointers, you should probably test for the different possible outcomes, rather than just verifying if the method has been called.

If your SUT is ignoring the error, then let the block you pass into the matcher keep a boolean flag to indicate that it was called like so:

- (void)testNotCaringAboutTheError {
    __block BOOL called = NO;
    matcher.errorSettingBlock = ^(NSError **error) {
        called = YES;
    };

    [[given([context doWithError:nil]) withMatcher:matcher] willReturn:nil];
    [sut call];
    assertThatBool(called, is(equalToBool(YES)));
}

Or with simple verification:

- (void)testWithVerifyOnly {
    [sut call];
    [[verify(context) withMatcher:matcher] doWithError:nil];
}

PS: Ignoring errors is probably something you don't want to do...

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Mike Seghers
  • 1,925
  • 2
  • 16
  • 12
  • Hi Mike, thanks for suggestion. So I think approach about `given` will work. But I think approach `verify` is also valid. As for my case I usually more than one test for particular method. I try to keep minimum one-two assertions per test. So that was test that was checking I save context before leaving the screen. And there will be another method that will check if error is propagated to user in case of error. But I'm in doubts since this is a case when something going wrong with NSManagedContext and after user pressed logout. You know how hard to suggest something to user in such errors – Eugen Martynov May 27 '14 at 07:47
  • Indeed, bubbling up the error to the user is not what I would do here. I would however "take note" of it, like simply logging it. The new challenge then is checking wether something has been logged. I'm not sure how to solve that one. I would then fall back to the `testNotCaringAboutTheError`scenario. You have a valid point when you say verify is a good scenario, but I don't thing OCMockito can handle this situation. – Mike Seghers May 27 '14 at 07:53
  • I researched a bit further and added an extra test to my answer. I think it solves what you want to accomplish. – Mike Seghers May 27 '14 at 08:21
  • Thanks a lot! I will use proposed approach and maybe I will contribute to OCMockito to simplify `verify` approach – Eugen Martynov May 27 '14 at 08:56
  • Hi Eugen, can you accept the answer? By the way, I think we could just use anything() instead of the custom matcher in the last example. – Mike Seghers May 27 '14 at 09:05
  • Hey Mike, just tried your approach and it works. Thank you! Accepted answer – Eugen Martynov May 27 '14 at 12:25