Unit Tests for designs that use notifications
Asked Answered
A

2

6

I'm having difficulty testing some logic that uses notifications. I've read about enforcing that particular NSNotifications are sent, but that doesn't really address the problem I'm seeing.

[SomeObject PerformAsyncOperation] creates an NSURLRequest and sets itself as the response delegate. Depending on the content of the response, SomeObject posts a success or failure NSNotification to the default NSNotificationCenter.

The problem with my test is that after PerformAsyncOperation is called, the test doesn't wait for the response to be sent. Instead, it continues on with the assert - which fails, because the request/response hasn't had time to be sent/received/parsed.

Here is the code:

-(void)testMethod {
    SomeObject *x = [[SomeObject alloc] init];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                        selector:@selector(success:)
                                            name:SomeObjectSuccess
                                          object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                        selector:@selector(failure:)
                                            name:SomeObjectFailure
                                          object:nil];

    operationCompleted = false; // declared in .h

    [x PerformAsyncOperation:@"parameter"];

    STAssertTrue(operationCompleted , @"Operation has completed.");

    [[NSNotificationCenter defaultCenter] removeObserver:self name:SomeObjectSuccess object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:SomeObjectFailure object:nil];

    [x release];
}

-(void) failure:(NSNotification *) notification {
   operationCompleted = true;
   NSLog(@"Received failure message.");
}

-(void) success:(NSNotification *) notification {
   operationCompleted = true;
   NSLog(@"Received success message.");
}

I've tried sleeping the thread after the call to PerformAsyncOperation, as well as an empty while (!operationCompleted) loop. Neither works - sleeping the thread still fails the assert, and the while loop never exits. I've also tried moving the asserts into success: and failure:, but because OCUnit expects each method to be a test method, it just executes all three methods immediately and quits (without waiting for the NSURLRequest's response, which is the whole point of the exercise!).

Any ideas or insight would be greatly appreciated. Thanks!

Anaplastic answered 1/6, 2011 at 17:47 Comment(0)
H
15

The problem you face is not notifications, which are synchronous. Rather, it is that you are firing off an asynchronous operation. To make this a repeatable test, you need to resynchronize things.

NSTimeInterval timeout = 2.0;   // Number of seconds before giving up
NSTimeInterval idle = 0.01;     // Number of seconds to pause within loop
BOOL timedOut = NO;

NSDate *timeoutDate = [[NSDate alloc] initWithTimeIntervalSinceNow:timeout];
while (!timedOut && !operationCompleted)
{
    NSDate *tick = [[NSDate alloc] initWithTimeIntervalSinceNow:idle];
    [[NSRunLoop currentRunLoop] runUntilDate:tick];
    timedOut = ([tick compare:timeoutDate] == NSOrderedDescending);
    [tick release];
}
[timeoutDate release];

Beyond this point, you know that either the operation completed, or the test timed out.

Hartzel answered 3/6, 2011 at 5:28 Comment(2)
I would highly recommend using an assertion framework that provides syntax sugar for you. For example, I use Specta github.com/specta/specta/tree/0.3-wip, which provides a function called waitUntil(^{}). You place your code within the block and if it doesn't complete within a configurable amount of time (10s by default) then the test fails. Otherwise it succeeds.Gladis
Yes, a number of testing frameworks provide this. Recent additions include Apple's XCTest and my OCHamcrest.Hartzel
F
0

For simplicity reasons, and if possible, use the synchronous network requests.

- (void)testExample
{
    [self measureBlock:^{

        NSURLRequest *request = [[NSURLRequest alloc] initWith...];

        NSHTTPURLResponse *response = nil;
        NSData *data = [NSURLConnection sendSynchronousRequest:request
                                         returningResponse:&response
                                                     error:nil];

        XCTAssertEqual(response.statusCode,200);

        // process the data from response ... call the delegate methods or whatever

        NSObject *object = [NSJSONSerialization JSONObjectWithData:data
                                                       options:NSJSONReadingMutableContainers
                                                         error:nil];
        XCTAssertTrue([object isKindOfClass:[NSArray class]]);
    }];
}
Farrago answered 11/6, 2015 at 9:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.