0

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:

  • communicator: object responsible for making the network calls
  • manager: instructs the communicator to make calls and then pushes result to its delegate.
  • delegate: manager delegate that conforms to the StackOverflowManagerDelegate protocol. gets the results and processes it.

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];
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
abbood
  • 23,101
  • 16
  • 132
  • 246

1 Answers1

1

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];
e1985
  • 6,239
  • 1
  • 24
  • 39
  • hey man thanks a lot for your answer! your first part worked like a charm.. however, your second part was a little off.. but it gave me the inspiration to solve the problem the correct way (it's not your fault, i'm sure you forgot what the second test looks like) I updated my question with the second unit test from the boom as well as the correct OCMock version of it. +1 if you like! :D – abbood Oct 07 '13 at 03:13
  • created a [repo](https://github.com/abbood/iOSTestDrivenDevelopmentWithOCMock/tree/master/BrowseOverflowTests) on github to put the OCMock equivalents of the examples of the book. – abbood Oct 07 '13 at 06:51
  • Have edited my answer. I was wrong when I said that NSError isEqual: returns NO when comparing equal errors but different instances... – e1985 Oct 07 '13 at 08:47
  • And it's nice to see you created a repo with the examples of the book. I think it may be very useful for people who read it and want to start using OCMock - though starting with "handmade" mocks is very good for learning purposes and, in some cases(few, but some), they may be a better option than mocks created with any library. – e1985 Oct 07 '13 at 08:52
  • can you mention such an example for when creating handmade mocks is better than the other kind? – abbood Oct 07 '13 at 09:15
  • For instance, in some cases you may encounter some problems working with C structs and mocking libs - search for it here in SO, there are several questions about that. In other cases, and this is more matter of personal taste, a handmade mock may feel more natural and easy to use. But to give you a real live example, in the project I am currently working on(almost 400 tests methods, and I am not working with structs at all) I am only using handmade mocks in some old tests where I didn't know (yet) how to work with block based APIs and OCMock. – e1985 Oct 07 '13 at 09:33
  • uha.. well as i was going over the examples in the said book.. it just seemed like an overkill to create those fake classes (well i guess i proved that they were.. b/c i pretty much did everything they do with OCMock).. one of the things i love about NSBlock is that you stop playing hide and seek with your code and almost see all your code where you expect it (rather than jumping back and forth between delegates etc).. that's what i like about OCmock.. you (almost) see all the code in the obdy of the unit test.. rather than having to create fake classes elsewhere.. – abbood Oct 07 '13 at 11:04
  • but your point is well taken about C structs and all.. thats' why i'm actually doing both fake classes and mocks.. (it also forces me to understand the examples better from the book.. otherwise i'm just cutting and pasting.. no head scratching there you know :p) – abbood Oct 07 '13 at 11:05
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/38717/discussion-between-e1985-and-abbood) – e1985 Oct 07 '13 at 11:30