Подтвердить что ты не робот

Тестирование асинхронного вызова в unit test в iOS

Я столкнулся с проблемой при модульном тестировании асинхронного вызова в iOS. (Хотя он отлично работает с контроллерами.)

Кто-нибудь сталкивался с этой проблемой раньше? Я попытался использовать функцию ожидания, но я все еще сталкиваюсь с той же проблемой.

Пожалуйста, предложите пример хорошего способа сделать это.

4b9b3361

Ответ 1

Попробуйте структуру KIWI. Он мощный и может помочь вам в других тестах.

Ответ 2

Вам нужно будет открутить runloop до вызова обратного вызова. Убедитесь, что он вызывается в главной очереди.

Попробуйте следующее:

__block BOOL done = NO;
doSomethingAsynchronouslyWithBlock(^{
    done = YES;
});

while(!done) {
   [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

Вы также можете использовать семафор (пример ниже), но я предпочитаю вращать runloop, чтобы асинхронные блоки отправлялись в основную очередь, которая должна быть обработана.

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
doSomethingAsynchronouslyWithBlock(^{
    //...
    dispatch_semaphore_signal(sem);
});

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

Ответ 3

Вот описание Apple собственной поддержки для асинхронного тестирования.

TL; руководство DR:

Посмотрите XCTextCase+AsynchronousTesting.h

Существует специальный класс XCTestExpectation только с одним открытым методом: - (void)fulfill;

Вы должны запустить экземпляр этого класса и в случае успешного вызова метод fulfill. В противном случае ваш тест завершится с ошибкой после таймаута, который вы укажете в этом методе:

- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(XCWaitCompletionHandler)handlerOrNil;

Пример:

- (void)testAsyncMethod
{

    //Expectation
    XCTestExpectation *expectation = [self expectationWithDescription:@"Testing Async Method Works Correctly!"];

    [MyClass asyncMethodWithCompletionBlock:^(NSError *error) {        
        if(error)
            NSLog(@"error is: %@", error);
        else
            [expectation fulfill];
    }];

    //Wait 1 second for fulfill method called, otherwise fail:    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {

        if(error)
        {
            XCTFail(@"Expectation Failed with error: %@", error);
        }

    }];
}

Ответ 4

Я думаю, что многие из предлагаемых решений в этой статье имеют проблему, что если асинхронная операция не завершена, флаг "done" никогда не будет установлен, и тест будет зависать вечно.

Я успешно использовал этот подход во многих своих тестах.

- (void)testSomething {
    __block BOOL done = NO;

    [obj asyncMethodUnderTestWithCompletionBlock:^{
        done = YES;
    }];

    XCTAssertTrue([self waitFor:&done timeout:2],
                   @"Timed out waiting for response asynch method completion");
}


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

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

Ответ 6

AGAsyncTestHelper - макрос C для написания модульных тестов с асинхронными операциями и работает как с SenTestingKit, так и с XCTest.

Простой и точка

- (void)testAsyncBlockCallback
{
    __block BOOL jobDone = NO;

    [Manager doSomeOperationOnDone:^(id data) {
        jobDone = YES; 
    }];

    WAIT_WHILE(!jobDone, 2.0);
}

Ответ 7

Здесь другая альтернатива, XCAsyncTestCase, которая хорошо работает с OCMock, если вам нужно ее использовать. Он основан на асинхронном тесте GHUnit, но вместо этого использует обычную инфраструктуру XCTest. Полностью совместим с Xcode Bots.

https://github.com/iheartradio/xctest-additions

Использование такое же, просто импорт и подкласс XCAsyncTestCase.

@implementation TestAsync
- (void)testBlockSample
{
    [self prepare];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
        sleep(1.0);
        [self notify:kXCTUnitWaitStatusSuccess];
    });
    // Will wait for 2 seconds before expecting the test to have status success
    // Potential statuses are:
    //    kXCTUnitWaitStatusUnknown,    initial status
    //    kXCTUnitWaitStatusSuccess,    indicates a successful callback
    //    kXCTUnitWaitStatusFailure,    indicates a failed callback, e.g login operation failed
    //    kXCTUnitWaitStatusCancelled,  indicates the operation was cancelled
    [self waitForStatus:kXCTUnitWaitStatusSuccess timeout:2.0];
}

Ответ 8

Сэм Бродкин уже дал правильный ответ .

Просто для того, чтобы ответ выглядел лучше с первого взгляда, я приведу здесь пример кода.

Используйте XCTest Expectation.

// Test that the document is opened. Because opening is asynchronous,
// use XCTestCase asynchronous APIs to wait until the document has
// finished opening.

- (void)testDocumentOpening
{
    // Create an expectation object.
    // This test only has one, but it possible to wait on multiple expectations.
    XCTestExpectation *documentOpenExpectation = [self expectationWithDescription:@"document open"];

    NSURL *URL = [[NSBundle bundleForClass:[self class]]
                            URLForResource:@"TestDocument" withExtension:@"mydoc"];
    UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
    [doc openWithCompletionHandler:^(BOOL success) {
        XCTAssert(success);
        // Possibly assert other things here about the document after it has opened...

        // Fulfill the expectation-this will cause -waitForExpectation
        // to invoke its completion handler and then return.
        [documentOpenExpectation fulfill];
    }];

    // The test will pause here, running the run loop, until the timeout is hit
    // or all expectations are fulfilled.
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        [doc closeWithCompletionHandler:nil];
    }];
}

Ответ 9

Я предлагаю вам взглянуть на тесты Facebook-ios-sdk. Это хороший пример того, как тестировать async unit test на iOS, хотя лично я считаю, что тесты асинхронного анализа должны быть подвергнуты анализу синхронизации.

FBTestBlocker: блокировщик, который предотвращает выходы текущего потока с заданным таймаутом. Вы можете перетащить это в свой проект, но вам нужно удалить связанные с OCMock вещи, если у вас этого нет в проекте.

FBTestBlocker.h

FBTestBlocker.m

FBURLConnectionTests: примеры тестов, на которые вы должны обратить внимание.

FBURLConnectionTests.h

FBURLConnectionTests.m

Этот фрагмент кода должен дать вам некоторую идею

- (void)testExample
{
    FBTestBlocker *_blocker = [[FBTestBlocker alloc] initWithExpectedSignalCount:1];
    __block BOOL excuted = NO;
    [testcase test:^(BOOL testResult) {
        XCTAssert(testResult, @"Should be true");
        excuted = YES;
        [_blocker signal];
    }];

    [_blocker waitWithTimeout:4];
    XCTAssertTrue(excuted, @"Not executed");
}

Ответ 10

Я рекомендую вам соединение семафора + runloop, я также написал метод, который принимает блок:

// Set the flag to stop the loop
#define FLEND() dispatch_semaphore_signal(semaphore);

// Wait and loop until flag is set
#define FLWAIT() WAITWHILE(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))

// Macro - Wait for condition to be NO/false in blocks and asynchronous calls
#define WAITWHILE(condition) \
do { \
while(condition) { \
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]]; \
} \
} while(0)

метод:

typedef void(^FLTestAsynchronousBlock)(void(^completion)(void));

void FLTestAsynchronous(FLTestAsynchronousBlock block) {
    FLSTART();
    block(^{
        FLEND();
    });
    FLWAIT();
};

и вызовите

FLTestAsynchronous(^(void(^completion)()){

    [networkManager signOutUser:^{
        expect(networkManager.currentUser).to.beNil();
        completion();
    } errorBlock:^(NSError *error) {
        expect(networkManager.currentUser).to.beNil();
        completion();
    }];

});

Ответ 12

вы можете использовать async api для вызова в swift, как это

private let serverCommunicationManager : ServerCommunicationManager = {
    let instance = ServerCommunicationManager()
    return instance
}()

var expectation:XCTestExpectation?
func testAsyncApiCall()  {
    expectation = self.expectation(description: "async request")

    let header = ["Authorization":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImQ4MmY1MTcxNzI4YTA5MjI3NWIzYWI3OWNkOTZjMGExOTI4MmM2NDEyZjMyYWQzM2ZjMzY4NmU2MjlhOWY2YWY1NGE0MDI4MmZiNzY2NWQ3In0.eyJhdWQiOiIxIiwianRpIjoiZDgyZjUxNzE3MjhhMDkyMjc1YjNhYjc5Y2Q5NmMwYTE5MjgyYzY0MTJmMzJhZDMzZmMzNjg2ZTYyOWE5ZjZhZjU0YTQwMjgyZmI3NjY1ZDciLCJpYXQiOjE1MDg4MjU1NTEsIm5iZiI6MTUwODgyNTU1MSwiZXhwIjoxNTQwMzYxNTUxLCJzdWIiOiIiLCJzY29wZXMiOltdfQ.osoMQgiY7TY7fFrh5r9JRQLQ6AZhIuEbrIvghF0VH4wmkqRUE6oZWjE5l0jx1ZpXsaYUhci6EDngnSTqs1tZwFTQ3srWxdXns2R1hRWUFkAN0ri32W0apywY6BrahdtiVZa9LQloD1VRMT1_QUnljMXKsLX36gXUsNGU6Bov689-bCbugK6RC3n4LjFRqJ3zD9gvkRaODuOQkqsNlS50b5tLm8AD5aIB4jYv3WQ4-1L74xXU0ZyBTAsLs8LOwvLB_2B9Qdm8XMP118h7A_ddLo9Cyw-WqiCZzeZPNcCvjymNK8cfli5_LZBOyjZT06v8mMqg3zszWzP6jOxuL9H1JjBF7WrPpz23m7dhEwa0a-t3q05tc1RQRUb16W1WhbRJi1ufdMa29uyhX8w_f4fmWdAnBeHZ960kjCss98FA73o0JP5F0GVsHbyCMO-0GOHxow3-BqyPOsmcDrI4ay006fd-TJk52Gol0GteDgdntvTMIrMCdG2jw8rfosV6BgoJAeRbqvvCpJ4OTj6DwQnV-diKoaHdQ8vHKe-4X7hbYn_Bdfl52gMdteb3_ielcVXIaHmQ-Dw3E2LSVt_cSt4tAHy3OCd7WORDY8uek4Paw8Pof0OiuqQ0EB40xX5hlYqZ7P_tXpm-W-8ucrIIxgpZb0uh-wC3EzBGPjpPD2j9CDo"]
    serverCommunicationManager.sendServerRequest(httpMethodType: .get, baseURL: "http://192.168.2.132:8000/api/v1/user-role-by-company-id/2", param: nil, header: header) { (isSuccess, msg , response) in
        if isSuccess
        {
            let array = response as! NSArray

            if  array.count == 8
            {
                XCTAssertTrue(true)
                self.expectation?.fulfill()
            }
            else
            {
                XCTAssertFalse(false)
                XCTFail("array count fail")
            }
        }
    }
    waitForExpectations(timeout: 5) { (error) in
        if let error = error{
            XCTFail("waiting with error: \(error.localizedDescription)")
        }
    }
}