4

I am attempting to write unit tests for an iOS application which makes use of the Parse backend framework, and after much experimentation seem to be failing at writing successful unit tests. I have found a few posts on testing asynchronous code (Testing asynchronous call in unit test in iOS) and testing network calls, but am yet to find a way of testing calls to the Parse backend with async callbacks.

To give an example, could anyone advise how I would test the following line of code:

[PFUser saveUser:userModelMock withBlock:^(BOOL success, NSError *error) {

}];

I am using OCMock and the XCTest framework.

Any help would be much appreciated.

* EDIT * This is what I have so far but seems to be failing

- (void)testSaveUser {
    id userModelMock = [OCMockObject niceMockForClass:[UserModel class]];
    id userControllerMock = [OCMockObject niceMockForClass:[self.userController class]];

    [[userModelMock expect] saveInBackgroundWithBlock:[OCMArg any]];
    [[userControllerMock stub] saveUser:userModelMock withBlock:[OCMArg any]];

    [userModelMock verify];
}
Community
  • 1
  • 1
Alex Brown
  • 1,613
  • 1
  • 13
  • 23
  • 1
    What is it that you are trying to test? PFUser hopefully has its own unit tests, so your tests should count on it operating as advertised and you can simply mock the call to saveUser:withBlock:. If you want to test that your asynchronous callback works as expected, this answer might help: http://stackoverflow.com/a/20694495/449161 – Ben Flynn Feb 15 '14 at 18:49
  • Agreed, I would imagine Parse have internal tests - however I am subclassing PFUser and have created a controller that handles all of the data interaction. Within the controller I have a method which calls saveUser, so in the test I need to verify saveUser: is called when I call the method that wraps it. – Alex Brown Feb 16 '14 at 21:22
  • If you are testing that the controller calls this method from another method, just use a mock and `expects` for the PFUser object and call the controller method from your test. If the call is asynchronous, follow the model from the post you linked. Or am I missing something? – Ben Flynn Feb 16 '14 at 21:45
  • Thats exactly what i'm doing. However I don't want the parse method to be called, I just want to know that it would be called. I am new to unit testing and still working through the learning curve. I'll edit the post and stick up what I have already. – Alex Brown Feb 17 '14 at 08:35

3 Answers3

4

If you don't mind to utilize a third party library, you may use the following approach:

#import <XCTest/XCTest.h>
#import <RXPromise/RXPromise.h>
...

@interface PFUserTests : XCTestCase
@end

@implementation PFUserTests

// helper:
- (RXPromise*) saveUser:(User*)user {
    RXPromise* promise = [[RXPromise alloc] init];
    [PFUser saveUser:user withBlock:^(Bool success, NSError*error){
        if (success) {
            [promise fulfillWithValue:@"OK"];
        }
        else {
            [promise rejectWithReason:error];
        }
    }];
    return promise;
}


// tests:

-(void) testSaveUser 
{
    User* user = ... ;
    RXPromise* promise = [self saveUser:user];

    // set a timeout:
    [promise setTimeout:5.0];

    [promise.thenOn(dispatch_get_main_queue(), ^id(id result) {
        XCTAssertTrue([result isEqualToString:@"OK"], @"");
        XCTAssertTrue( ... , @"");
        return nil;
    }, ^id(NSError* error) {
        // this may fail either due to a timeout or when saveUser fails:
        XCTFail(@"Method failed with error: %@", error);
        return nil;
    }) runLoopWait];  // suspend the run loop, until after the promise gets resolved 

}

A few notes:

You can execute the success or failure handler (which are registered for the promise with the thenOn statement) on any queue, just specify the queue. This does not dead lock the main thread, even though the handler gets explicitly executed on the main thread, and the test runs as well on the main thread (thanks to the run loop mechanism).

The method runLoopWait will enter a run loop, and only return once the promise has been resolved. This is both, effective and efficient.

A promise accepts a timeout. If the timeout expires, the promise will be "rejected" (that is resolved) with a corresponding error.

IMO, promises are an invaluable tool for handling common asynchronous programming problems - not just a helper for unit tests. (See also: wiki article Futures and promises.

Please note, I'm the author of the RXPromise library, and thus I'm totally biased ;)

There are other Promise implementations for Objective-C as well.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • Thanks for this, I had a look and RXPromise looks like a really useful framework so good job with that. Perhaps I didn't explain myself fully; I don't want to call the saveUser:withBlock: method directly as it makes a network call which is obviously not ideal for testing. IS there a way I can mock this? Thanks again. – Alex Brown Feb 14 '14 at 11:40
  • No need for 3rd party libs - you can use XCTestExpectation. Tutorial available on NSHipster: http://nshipster.com/xctestcase/ – Paul Ardeleanu Apr 17 '15 at 13:24
  • @PaulArdeleanu Thanks for the update. XCTestExpectation was not available at this time. Still IMHO, promises are easier to use and more versatile than XCTestExpectation. – CouchDeveloper Apr 20 '15 at 08:16
1

Turns out it was just due to my lack of unit testing understanding. After some research on mock objects, stubs and the like I came up with:

- (void)testSaveUser {
    id userModelMock = [OCMockObject mockForClass:[UserModel class]];
    id userControllerMock = [OCMockObject partialMockForObject:self.userController];

    [[userModelMock expect] saveInBackgroundWithBlock:[OCMArg any]];
    [userControllerMock saveUser:userModelMock withBlock:[OCMArg any]];

    [userModelMock verify];
}
Alex Brown
  • 1,613
  • 1
  • 13
  • 23
  • 1
    Yes, it looks like initially you weren't calling the method you wanted to test! I presume `self.userController` is returning a user controller object created by your test class for each test. Glad you are making progress. – Ben Flynn Feb 17 '14 at 18:56
  • Just to say that passing [OCMArg any] as a block in the method you want to test doesn't make sense. Instead pass a fake block or nil, depending what you want to test – e1985 Feb 18 '14 at 02:11
1

With the macro WAIT_WHILE(<expression>, <time_limit>) in https://github.com/hfossli/AGAsyncTestHelper/ you can write

- (void)testSaveUser
{
    __block BOOL saved = NO;
    [PFUser saveUser:userModelMock withBlock:^(BOOL success, NSError *error) {
        saved = YES;
    }];
    WAIT_WHILE(!saved, 10.0, @"Failed to save user within 10 seconds timeframe);
}
hfossli
  • 22,616
  • 10
  • 116
  • 130