IT TIP

비동기 API를 단위 테스트하는 방법은 무엇입니까?

itqueen 2020. 11. 19. 22:43
반응형

비동기 API를 단위 테스트하는 방법은 무엇입니까?


내가 설치 한 Mac 용 구글 도구 상자를 엑스 코드로 발견 단위 테스트를 설정하는 지침에 따라 여기 .

모두 훌륭하게 작동하며 모든 개체에서 동기 메서드를 완벽하게 테스트 할 수 있습니다. 그러나 실제로 테스트하려는 대부분의 복잡한 API는 대리자에 대한 메서드 호출을 통해 비동기 적으로 결과를 반환합니다. 예를 들어 파일 다운로드 및 업데이트 시스템에 대한 호출은 즉시 반환 된 다음 파일 다운로드가 완료되면 -fileDownloadDidComplete : 메서드를 실행합니다. .

이것을 단위 테스트로 어떻게 테스트합니까?

testDownload 함수를 원하거나 적어도 테스트 프레임 워크가 fileDownloadDidComplete : 메소드가 실행될 때까지 '대기'하고 싶은 것 같습니다.

편집 : 이제 XCode 내장 XCTest 시스템을 사용하도록 전환 했으며 Github의 TVRSMonitor 가 세마포어를 사용하여 비동기 작업이 완료 될 때까지 기다릴 수있는 아주 쉬운 방법을 제공 한다는 사실발견했습니다 .

예를 들면 :

- (void)testLogin {
  TRVSMonitor *monitor = [TRVSMonitor monitor];
  __block NSString *theToken;

  [[Server instance] loginWithUsername:@"foo" password:@"bar"
                               success:^(NSString *token) {
                                   theToken = token;
                                   [monitor signal];
                               }

                               failure:^(NSError *error) {
                                   [monitor signal];
                               }];

  [monitor wait];

  XCTAssert(theToken, @"Getting token");
}

나는 같은 질문에 부딪 쳤고 저에게 맞는 다른 해결책을 찾았습니다.

다음과 같이 세마포어를 사용하여 비동기 작업을 동기화 흐름으로 전환하기 위해 "구식"접근 방식을 사용합니다.

// create the object that will perform an async operation
MyConnection *conn = [MyConnection new];
STAssertNotNil (conn, @"MyConnection init failed");

// create the semaphore and lock it once before we start
// the async operation
NSConditionLock *tl = [NSConditionLock new];
self.theLock = tl;
[tl release];    

// start the async operation
self.testState = 0;
[conn doItAsyncWithDelegate:self];

// now lock the semaphore - which will block this thread until
// [self.theLock unlockWithCondition:1] gets invoked
[self.theLock lockWhenCondition:1];

// make sure the async callback did in fact happen by
// checking whether it modified a variable
STAssertTrue (self.testState != 0, @"delegate did not get called");

// we're done
[self.theLock release]; self.theLock = nil;
[conn release];

호출하십시오

[self.theLock unlockWithCondition:1];

그럼 대의원에서.


이 질문에 대해 거의 1 년 전에 질문하고 답변 한 것에 감사하지만 주어진 답변에 동의하지 않을 수밖에 없습니다. 비동기 작업, 특히 네트워크 작업을 테스트하는 것은 매우 일반적인 요구 사항이며 올바르게 작동하는 데 중요합니다. 주어진 예에서 실제 네트워크 응답에 의존하면 테스트의 중요한 가치 중 일부를 잃게됩니다. 특히 테스트는 통신중인 서버의 가용성과 기능적 정확성에 따라 달라집니다. 이 종속성은 테스트를

  • 더 취약 함 (서버가 다운되면 어떻게됩니까?)
  • 덜 포괄적 (실패 응답 또는 네트워크 오류를 어떻게 지속적으로 테스트합니까?)
  • 이것을 테스트하는 것이 훨씬 느립니다.

단위 테스트는 순식간에 실행되어야합니다. 테스트를 실행할 때마다 몇 초의 네트워크 응답을 기다려야하는 경우 테스트를 자주 실행할 가능성이 적습니다.

단위 테스트는 주로 종속성을 캡슐화하는 것입니다. 테스트중인 코드의 관점에서 두 가지가 발생합니다.

  1. 귀하의 메소드는 NSURLConnection을 인스턴스화하여 네트워크 요청을 시작합니다.
  2. 지정한 대리자는 특정 메서드 호출을 통해 응답을받습니다.

델리게이트는 원격 서버의 실제 응답이든 테스트 코드이든 응답이 어디에서 왔는지 신경 쓰지 않습니다. 이를 활용하여 단순히 응답을 직접 생성하여 비동기 작업을 테스트 할 수 있습니다. 테스트가 훨씬 더 빠르게 실행되고 성공 또는 실패 응답을 안정적으로 테스트 할 수 있습니다.

이것은 작업중인 실제 웹 서비스에 대해 테스트를 실행해서는 안된다는 의미가 아니라 통합 테스트이며 자체 테스트 스위트에 속합니다. 해당 제품군의 실패는 웹 서비스가 변경되었거나 단순히 다운되었음을 의미 할 수 있습니다. 더 취약하기 때문에 자동화는 단위 테스트를 자동화하는 것보다 가치가 낮은 경향이 있습니다.

네트워크 요청에 대한 비동기 응답을 테스트하는 방법과 관련하여 몇 가지 옵션이 있습니다. 메소드를 직접 호출하여 간단히 델리게이트를 테스트 할 수 있습니다 (예 : [someDelegate connection : connection didReceiveResponse : someResponse]). 이것은 다소 작동하지만 약간 잘못되었습니다. 객체가 제공하는 델리게이트는 특정 NSURLConnection 객체에 대한 델리게이트 체인의 여러 객체 중 하나 일 수 있습니다. 델리게이트의 메서드를 직접 호출하면 체인의 상위에있는 다른 델리게이트가 제공하는 일부 핵심 기능이 누락 될 수 있습니다. 더 나은 대안으로 생성 한 NSURLConnection 객체를 스텁하고 전체 델리게이트 체인에 응답 메시지를 보내도록 할 수 있습니다. NSURLConnection (다른 클래스 중에서)을 다시 열고이를 수행하는 라이브러리가 있습니다. 예를 들면 :https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m


St3fan, 당신은 천재입니다. 감사합니다!

이것이 당신의 제안을 사용하여 한 방법입니다.

'Downloader'는 완료시 실행되는 DownloadDidComplete 메소드로 프로토콜을 정의합니다. 런 루프를 종료하는 데 사용되는 BOOL 멤버 변수 'downloadComplete'가 있습니다.

-(void) testDownloader {
 downloadComplete = NO;
 Downloader* downloader = [[Downloader alloc] init] delegate:self];

 // ... irrelevant downloader setup code removed ...

 NSRunLoop *theRL = [NSRunLoop currentRunLoop];

 // Begin a run loop terminated when the downloadComplete it set to true
 while (!downloadComplete && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}


-(void) DownloaderDidComplete:(Downloader*) downloader withErrors:(int) errors {
    downloadComplete = YES;

    STAssertNotEquals(errors, 0, @"There were errors downloading!");
}

런 루프는 물론 영원히 계속 될 수 있습니다. 나중에 개선하겠습니다!


비동기 API를 쉽게 테스트 할 수있는 작은 도우미를 작성했습니다. 먼저 도우미 :

static inline void hxRunInMainLoop(void(^block)(BOOL *done)) {
    __block BOOL done = NO;
    block(&done);
    while (!done) {
        [[NSRunLoop mainRunLoop] runUntilDate:
            [NSDate dateWithTimeIntervalSinceNow:.1]];
    }
}

다음과 같이 사용할 수 있습니다.

hxRunInMainLoop(^(BOOL *done) {
    [MyAsyncThingWithBlock block:^() {
        /* Your test conditions */
        *done = YES;
    }];
});

이 경우에만 계속 done되고 TRUE있으므로, 반드시 한번 완료 설정 할 수 있습니다. 물론 원하는 경우 도우미에 시간 제한을 추가 할 수 있습니다.


이것은 까다 롭습니다. 테스트에서 runloop를 설정하고 해당 runloop를 비동기 코드에 지정하는 기능도 필요하다고 생각합니다. 그렇지 않으면 콜백이 실행 루프에서 실행되기 때문에 콜백이 발생하지 않습니다.

나는 당신이 루프에서 짧은 기간 동안 runloop를 실행할 수 있다고 생각합니다. 그리고 콜백이 공유 상태 변수를 설정하도록합니다. 또는 단순히 콜백을 요청하여 runloop를 종료 할 수도 있습니다. 그렇게하면 테스트가 끝났다는 것을 알 수 있습니다. 특정 시간이 지나면 루프를 중지하여 타임 아웃을 확인할 수 있어야합니다. 이 경우 시간 초과가 발생했습니다.

나는 이것을 한 적이 없지만 곧 나는 생각해야 할 것입니다. 결과를 공유 해주세요 :-)


AFNetworking 또는 ASIHTTPRequest와 같은 라이브러리를 사용하고 NSOperation (또는 해당 라이브러리가있는 하위 클래스)을 통해 요청을 관리하는 경우 NSOperationQueue를 사용하여 테스트 / 개발 서버에 대해 쉽게 테스트 할 수 있습니다.

테스트 중 :

// create request operation

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:request];
[queue waitUntilAllOperationsAreFinished];

// verify response

이것은 기본적으로 작업이 완료 될 때까지 runloop를 실행하여 모든 콜백이 정상적으로 백그라운드 스레드에서 발생하도록합니다.


@ St3fan의 솔루션에 대해 자세히 설명하려면 요청을 시작한 후 다음을 시도 할 수 있습니다.

- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs
{
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];

    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
        if ([timeoutDate timeIntervalSinceNow] < 0.0)
        {
            break;
        }
    }
    while (!done);

    return done;
}

또 다른 방법:

//block the thread in 0.1 second increment, until one of callbacks is received.
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];

    //setup timeout
    float waitIncrement = 0.1f;
    int timeoutCounter  = (int)(30 / waitIncrement); //30 sec timeout
    BOOL controlConditionReached = NO;


    // Begin a run loop terminated when the downloadComplete it set to true
    while (controlConditionReached == NO)
    {

        [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:waitIncrement]];
        //control condition is set in one of your async operation delegate methods or blocks
        controlConditionReached = self.downloadComplete || self.downloadFailed ;

        //if there's no response - timeout after some time
        if(--timeoutCounter <= 0)
        {
            break;
        }
    }

https://github.com/premosystems/XCAsyncTestCase 를 사용하는 것이 매우 편리합니다.

XCTestCase에 세 가지 매우 편리한 메서드를 추가합니다.

@interface XCTestCase (AsyncTesting)

- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout;
- (void)waitForTimeout:(NSTimeInterval)timeout;
- (void)notify:(XCTAsyncTestCaseStatus)status;

@end

매우 깨끗한 테스트를 허용합니다. 프로젝트 자체의 예 :

- (void)testAsyncWithDelegate
{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
    [NSURLConnection connectionWithRequest:request delegate:self];
    [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Request Finished!");
    [self notify:XCTAsyncTestCaseStatusSucceeded];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"Request failed with error: %@", error);
    [self notify:XCTAsyncTestCaseStatusFailed];
}

Thomas Tempelmann이 제안한 솔루션을 구현했으며 전반적으로 잘 작동합니다.

However, there is a gotcha. Suppose the unit to be tested contains the following code:

dispatch_async(dispatch_get_main_queue(), ^{
    [self performSelector:selector withObject:nil afterDelay:1.0];
});

The selector may never be called as we told the main thread to lock until the test completes:

[testBase.lock lockWhenCondition:1];

Overall, we could get rid of the NSConditionLock altogether and simply use the GHAsyncTestCase class instead.

This is how I use it in my code:

@interface NumericTestTests : GHAsyncTestCase { }

@end

@implementation NumericTestTests {
    BOOL passed;
}

- (void)setUp
{
    passed = NO;
}

- (void)testMe {

    [self prepare];

    MyTest *test = [MyTest new];
    [test run: ^(NSError *error, double value) {
        passed = YES;
        [self notify:kGHUnitWaitStatusSuccess];
    }];
    [test runTest:fakeTest];

    [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0];

    GHAssertTrue(passed, @"Completion handler not called");
}

Much cleaner and doesn't block the main thread.


I just wrote a blog entry about this (in fact I started a blog because I thought this was an interesting topic). I ended up using method swizzling so I can call the completion handler using any arguments I want without waiting, which seemed good for unit testing. Something like this:

- (void)swizzledGeocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler
{
    completionHandler(nil, nil); //You can test various arguments for the handler here.
}

- (void)testGeocodeFlagsComplete
{
    //Swizzle the geocodeAddressString with our own method.
    Method originalMethod = class_getInstanceMethod([CLGeocoder class], @selector(geocodeAddressString:completionHandler:));
    Method swizzleMethod = class_getInstanceMethod([self class], @selector(swizzledGeocodeAddressString:completionHandler:));
    method_exchangeImplementations(originalMethod, swizzleMethod);

    MyGeocoder * myGeocoder = [[MyGeocoder alloc] init];
    [myGeocoder geocodeAddress]; //the completion handler is called synchronously in here.

    //Deswizzle the methods!
    method_exchangeImplementations(swizzleMethod, originalMethod);

    STAssertTrue(myGeocoder.geocoded, @"Should flag as geocoded when complete.");//You can test the completion handler code here. 
}

blog entry for anyone that cares.


Looks like Xcode 6 will solve the issue. https://developer.apple.com/library/prerelease/ios/documentation/DeveloperTools/Conceptual/testing_with_xcode/testing_3_writing_test_classes/testing_3_writing_test_classes.html


My answer is that unit testing, conceptually, is not suitable for testing asynch operations. An asynch operation, such as a request to the server and the handling of the response, happens not in one unit but in two units.

To relate the response to the request you must either somehow block execution between the two units, or maintain global data. If you block execution then your program is not executing normally, and if you maintain global data you have added extraneous functionality that may itself contain errors. Either solution violates the whole idea of unit testing and requires you to insert special testing code into your application; and then after your unit testing, you will still have to turn off your testing code and do old-fashioned "manual" testing. The time and effort spent on unit testing is then at least partly wasted.


I found this article on this which is a muc http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/

참고URL : https://stackoverflow.com/questions/2162213/how-to-unit-test-asynchronous-apis

반응형