Testing of asynchronous requests

Asked

Viewed 272 times

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");
        }
    }];
}

2 answers

2


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

I took the initiative to interpret the question as a question of general competition, where one thread has to wait for another. So, be aware that when you need to implement a similar system in a serious environment.


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

A preferred solution will be to use a condition variable. This can be done with an instance of Nscondition.

The code below numbs the thread until the asynchronous call ends. 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 to the code:

  1. Create the condition variable.
  2. Start the asynchronous call.
  3. Start a timer for the timeout.
  4. Numb the thread until it receives a signal (5).
  5. Flag the dormant thread to run again. It’s okay to flag a condition that has no threads waiting.

Heed

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 you that that while cycle is consuming processing cycles which could be used for other types of computations. As such, it can affect analysis results, for example.

If memory serves me correctly, what spends processing cycles on that while: - Check the variable value bloqueado; in machine language is to see if the value pointed by a memory address is different from or equal to zero. - Check the variable value contador; on the one hand, it’s less bad than bloqueado because the compiler optimization will result in that variable staying on record, however, it is more complicated to check values of non-integer numbers. - Make a series of calls to the methods dateWithTimeIntervalSinceNow:, runMode:beforeDate: and currentRunLoop; This is a little more complicated than it seems, because in Objective-C the programmer does not call methods, the programmer sends messages; in run-time, the system will have to translate these messages into method calls and only then call them. (This concept of sending messages is what gives power to Cocoa and Cocoa-Touch, a little search for method swizzling and key-value Observing to understand some of the ramifications. ) - Increment the variable value contador.

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


I would also advise you to give a reading to Threading Programming Guide for three reasons:

  1. I may not have explained it correctly; I have a general idea of how it works, but I don’t know the details by heart.
  2. This may not be the best solution either; if instead of responding to a POST you were developing, you would re-read the guide and study what the best solution is. The best solution to one problem may not be the best for another.
  3. In my opinion, it is of utmost importance to understand how the platform manages the execution of the various threads, and what mechanisms exist to resolve competition issues; not as the one presented in the question, but others where it is essential to find the most efficient solution.

I hope I have been informative, and that this answer will help you in the future ;)


Update

Based on Bavarious' 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 great improvement as it allows blocks to be executed by the same thread that runs the function. This is not possible with the original solution of this answer.

I admit that I did not read the documentation in detail, but from what I understood Bavarious' solution does not numb the thread, but "blocks" the execution. What this means is, the execution context is "dormant" and the thread is left to perform 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 "dormant".

More information on Cfrunloop Apple documentation.

Thank you Bavarious.

  • 1

    A suggestion to improve your answer: instead of using NSCondition, you could just call CFRunLoopRun() in item 4. This function runs the loop indefinitely. For the run loop to be stopped when the block is executed or the timeout occurs (item 5), call CFRunLoopStop(CFRunLoopGetCurrent()). The code gets simpler.

  • @Bavarious Thank you for the reminder. I have updated the reply ;)

  • @rtiago42 Thank you for your reply.

2

I found how to force a test to keep your thread running and making asynchronous requests on Octestcase

Basically, it is to force the "lock" of thread through a Runloop and release it after the asynchronous response, or the firing of a timeout. Thus, the test thread is retained and the test is only given as success or failure after a timeout or there is an answer.

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");
}

Browser other questions tagged

You are not signed in. Login or sign up in order to post.