iosobjective-cunit-testingtddocmock

How to mock a property of an object conforming to a protocol and mocked by OCMock?


I'm following the examples in Test driven iOS Development and in one case there is a unit test that ensures that a delegate gets a 'dumbed down' version of an error method. So without going into too many details here are the relevant objects:

so this is what the test is:

@implementation QuestionCreationTests

{
    @private
    StackOverflowManager *mgr;
}
- (void)testErrorReturnedToDelegateIsNotErrorNotifiedByCommunicator {
    MockStackOverflowManagerDelegate *delegate =
    [[MockStackOverflowManagerDelegate alloc] init];
    mgr.delegate = delegate;
    NSError *underlyingError = [NSError errorWithDomain: @"Test domain"
                                                   code: 0 userInfo: nil];
    [mgr searchingForQuestionsFailedWithError: underlyingError];
    XCTAssertFalse(underlyingError == [delegate fetchError],
                  @"Error should be at the correct level of abstraction");
}

this is the implementation of searchingForQuestionsFailedWithError in StackOverflowManager, where the manager simply dumbs down the original error returned by the communicator and sends the dumbed down version to the delegate.

- (void)searchingForQuestionsFailedWithError:(NSError *)error {
    NSDictionary *errorInfo = [NSDictionary dictionaryWithObject: error
                                                          forKey: NSUnderlyingErrorKey];
    NSError *reportableError = [NSError
                                errorWithDomain: StackOverflowManagerSearchFailedError
                                code: StackOverflowManagerErrorQuestionSearchCode
                                userInfo:errorInfo];
    [delegate fetchingQuestionsOnTopic: nil
                       failedWithError: reportableError];
}

the author suggests that for this to work.. we actually have to create a mock object for the manager delegate like so:

@interface MockStackOverflowManagerDelegate : NSObject <StackOverflowManagerDelegate>
@property (strong) NSError *fetchError;

@end

@implementation MockStackOverflowManagerDelegate

@synthesize fetchError;

- (void)fetchingQuestionsOnTopic: (Topic *)topic
                 failedWithError: (NSError *)error {
    self.fetchError = error;
}

@end

this is the declaration of StackOverflowManagerDelegate:

@protocol StackOverflowManagerDelegate <NSObject>    
- (void)fetchingQuestionsOnTopic: (Topic *)topic
                 failedWithError: (NSError *)error {    
@end

Question: I've been going over all the examples of the book and trying to use OCMock instead of the manually made ones like the author is doing.. (i just thought it would be a lot less time consuming). Everything has worked so far.. but i'm stuck here.. how do I fake a property called fetchError on delegate? This is what I have right now:

- (void)testErrorReturnedToDelegateIsNotErrorNotifiedByCommunicator {
    id <StackOverflowManagerDelegate> delegate = 
       [OCMockObject mockForProtocol:@protocol(StackOverflowManagerDelegate)];
    mgr.delegate = delegate;

    NSError *underlyingError = [NSError errorWithDomain: @"Test domain"
                                                   code: 0 userInfo: nil];

    [mgr searchingForQuestionsFailedWithError: underlyingError];

    // compiler error here: no known instance method for selector 'fetchError'
    XCTAssertFalse(underlyingError == [mgr.delegate fetchError], @"error ");
}

In the guts of manager, manager calls fetchingQuestionsOnTopic on the delegate.. I know I can fake that method by using [[[delegate stub] andCall:@selector(differentMethod:) onObject:differentObject] fetchingQuestionsOnTopic:[OCMArg any]] where differentMethod would do whatever I want it to do.. I just don't know what to do with the result of differentMethod: I don't know how to store it in a mocked out property of delegate.


update: as a follow up to the answer below.. here is the implementation of unit test that ensures that the underlying error is still made available to the delegate:

- (void)testErrorReturnedToDelegateDocumentsUnderlyingError {
    MockStackOverflowManagerDelegate *delegate =
    [[MockStackOverflowManagerDelegate alloc] init];
    mgr.delegate = delegate;
    NSError *underlyingError = [NSError errorWithDomain: @"Test domain"
                                                   code: 0 userInfo: nil];
    [mgr searchingForQuestionsFailedWithError: underlyingError];
    XCTAssertEqual([[[delegate fetchError] userInfo]
                          objectForKey: NSUnderlyingErrorKey], underlyingError,
                         @"The underlying error should be available to client code");
}

and here is the OCMock version of it:

- (void)testErrorReturnedToDelegateDocumentsUnderlyingErrorOCMock {
    id delegate =
    [OCMockObject mockForProtocol:@protocol(StackOverflowManagerDelegate)];
    mgr.delegate = delegate;

    NSError *underlyingError = [NSError errorWithDomain: @"Test domain"
                                                   code: 0 userInfo: nil];

    [[delegate expect] fetchingQuestionsFailedWithError:
       [OCMArg checkWithBlock:^BOOL(id param) {

        return ([[param userInfo] objectForKey:NSUnderlyingErrorKey] == underlyingError);

    }]];

    [mgr searchingForQuestionsFailedWithError: underlyingError];

    [delegate verify];
}

Solution

  • Try this:

    - (void)testErrorReturnedToDelegateIsNotErrorNotifiedByCommunicator {
        id delegate = 
           [OCMockObject mockForProtocol:@protocol(StackOverflowManagerDelegate)];
        mgr.delegate = delegate;
    
        NSError *underlyingError = [NSError errorWithDomain: @"Test domain"
                                                       code: 0 userInfo: nil];
    
        [[delegate expect] fetchingQuestionsOnTopic:OCMOCK_ANY 
                                    failedWithError:[OCMArg isNotEqual:underlyingError]];
    
        [mgr searchingForQuestionsFailedWithError:underlyingError];
    
        [delegate verify];
    }
    

    To test the domain, code and userInfo of the error reported by the manager (if I remember well it's another test case in that book - I read it long time ago) you could do something like this:

        id <StackOverflowManagerDelegate> delegate = 
           [OCMockObject mockForProtocol:@protocol(StackOverflowManagerDelegate)];
        mgr.delegate = delegate;
    
        NSError *underlyingError = [NSError errorWithDomain: @"Test domain"
                                                       code: 0 userInfo: nil];
    
        NSError *expectedError = [NSError errorWithDomain:@"expected domain" 
                                                     code:0/* expected code here*/ 
                                                 userInfo:@{NSUnderlyingErrorKey: underlyingError}];
        [[delegate expect] fetchingQuestionsOnTopic:OCMOCK_ANY 
                                    failedWithError:expectedError];
    
        [mgr searchingForQuestionsFailedWithError: underlyingError];
    
        [delegate verify];