Testing asynchronous requests

4

How can I make asynchronous requests in iOS TestCases? Example:

-(void)test
{
    UserModel* user = [UserModel sharedInstance];
    [user requestUserInformationWithCompletion:^(NSError* error, NSDictionary* info){
        if(error)
        {
            STAssertTrue(error == nil, @"Erro no resultado");
        }else
        {
            STAssertTrue([[info objectForKey@"Nome"] isEqualToString:user.name], @"Usuário inválido");
        }
    }];
}
    
asked by anonymous 11.02.2014 / 16:55

2 answers

2

This solution is not the best. Although in your case, where you are only running tests, serve.

I took the initiative to interpret the question as a matter of general competition, where one thread has to wait for another. So you're alerted when you need to deploy a similar system in a serious environment.

The idea is correct, what you want is to 'hang' the thread. However, when the run loop executes this while loop it will be spending time it could use with another thread.

A preferred solution is to use a condition variable. This can be done with an instance of NSCondition . p>

The code below numbs the thread until the asynchronous call terminates. Unlike your solution, here the 'locked' thread does not run until it is unlocked.

- (void) test
{
    // 1
    NSCondition *condition = [NSCondition new];
    [condition lock];
    // 2
    UserModel* user = [UserModel sharedInstance];
    [user requestUserInformationWithCompletion:^(NSError* error, NSDictionary* info){
        if(error) {
            STFail(@"Erro no resultado: %@",error.debugDescription);
        } else {
            STAssertTrue([[info objectForKey@"Nome"] isEqualToString:user.name], @"Usuário inválido");
        }
        [condition lock];
        [condition signal]; // 5
        [condition unlock];
    }];
    // 3
    double timeout = 10.0;
    dispatch_time_t timeout_time = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC);
    dispatch_after(timeout_time, dispatch_get_main_queue(), ^(void){
        [condition lock];
        [condition signal]; // 5
        [condition unlock];
    });
    // 4
    [condition wait];
    [condition unlock];
    STAssertTrue(!bloqueado, @"O teste terminou por time out");
}

A brief explanation of the code:

  • Create the condition variable.
  • Start the asynchronous call.
  • Start a timer for timeout .
  • Fall asleep to receive a signal (5).
  • Flag the sleeping thread to re-run. It's okay to flag a condition that does not have threads waiting.
  • Attention

    For this particular problem, the solution is not at all relevant. And the use of a while serves.

    With this answer, I just want to warn that this while loop is consuming processing cycles that could be used for other types of computations. As such, it may affect analysis results, for example.

    If memory does not fail me, it spends processing cycles in this while:  - Check the value of the variable bloqueado ; in the machine language is to see if the value pointed to by a memory address is different or equal to zero.  - Check the value of the variable contador ; on the one hand, is less bad than bloqueado because compiler optimization will result in this variable being kept in register, however, it is more complicated to check non-integer numbers.  - Make a series of calls to methods dateWithTimeIntervalSinceNow: , runMode:beforeDate: and currentRunLoop ; this is a bit more complicated than it sounds, because in Objective-C the programmer does not call methods, the programmer sends messages; at run-time, the system will have to translate those messages into method calls and only then call them. (This messaging concept is what powers Cocoa and Cocoa-Touch, do some research by method swizzling and key-value observing to understand some of the ramifications.)  - Increase the value of the contador variable.

    And that's just what your code does; there is still what the system does for you.

    I also advise you to read Threading Programming Guide for three reasons:

  • I may not have explained correctly; I have a general idea of how this works, but I do not know the color details.
  • This may not be the best solution either; if instead of responding to a POST you were developing, you would reread the guide and study the best solution. The best solution to one problem may not be the best for another.
  • In my opinion, it is extremely important to understand how the platform manages the execution of the various threads, and what mechanisms exist to solve competition issues; not like the one you present in the question, but others where it is imperative to find the most efficient solution.
  • I hope to have been informative, and may this answer help you in the future;)

    Update

    Based on Bavarious's comment, I present an improvement:

    - (void) test
    {
        CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    
        UserModel* user = [UserModel sharedInstance];
        [user requestUserInformationWithCompletion:^(NSError* error, NSDictionary* info){
            if(error) {
                STFail(@"Erro no resultado: %@",error.debugDescription);
            } else {
                STAssertTrue([[info objectForKey@"Nome"] isEqualToString:user.name], @"Usuário inválido");
            }
            CFRunLoopStop(runLoop);
        }];
    
        double timeout = 10.0;
        dispatch_time_t timeout_time = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC);
        dispatch_after(timeout_time, dispatch_get_main_queue(), ^(void){
            CFRunLoopStop(runLoop);
        });
    
        CFRunLoopRun();
        STAssertTrue(!bloqueado, @"O teste terminou por time out");
    }
    

    This is a big improvement as it allows the blocks to be executed by the same thread that executes the function. This is not possible with the original solution of this response.

    I admit that I did not read the documentation in detail, but from what I noticed the Bavarious solution does not numb the thread, but "blocks" execution. What this means is, the execution context is "asleep" and the thread is left to execute other contexts. It is similar to Igor's solution, as the run loop continues to run, but the system tries not to execute the context that is "asleep."

    More information on Apple's CFRunLoop documentation .

    Thank you Bavarious.

        
    15.05.2014 / 11:06
    2

    I found how to force a test to keep its thread running and make asynchronous requests on the OCTestCase

    Basically, it is forcing the thread to crash through a RunLoop and releasing it after the asynchronous response, or triggering a timeout . Thus, the test thread is retained and the test is only given as success or failure after a timeout occurs or there is a response.

    Using the same question code as an example:

    - (void) test
    {
        __block BOOL bloqueado = YES;
        CGFloat contador = 0.0f;
    
        UserModel* user = [UserModel sharedInstance];
        [user requestUserInformationWithCompletion:^(NSError* error, NSDictionary* info) {
            if(error) {
                STFail(@"Erro no resultado: %@",error.debugDescription);
            } else {
                STAssertTrue([[info objectForKey@"Nome"] isEqualToString:user.name], @"Usuário inválido");
            }
            bloqueado = NO;
        }];
    
        while(bloqueado && contador < 10.0f) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
            contador += 0.1f;
        };
    
        STAssertTrue(!bloqueado,@"O teste terminou por time out");
    }
    
        
    14.02.2014 / 15:32