13

在 iOS 中对异步调用进行单元测试时遇到问题。(尽管它在视图控制器中运行良好。)

有没有人遇到过这个问题?我尝试过使用等待功能,但我仍然面临同样的问题。

请提出一个这样做的好方法的例子。

4

12 回答 12

28

在调用回调之前,您需要旋转运行循环。不过,请确保它在主队列上被调用。

尝试这个:

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

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

您也可以使用信号量(下面的示例),但我更喜欢旋转运行循环以允许处理分派到主队列的异步块。

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

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
于 2013-03-25T06:55:01.790 回答
14

这是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);
        }

    }];
}
于 2014-12-12T13:26:51.297 回答
8

我认为这篇文章中许多建议的解决方案都有一个问题,即如果异步操作未完成,则永远不会设置“完成”标志,并且测试将永远挂起。

我在许多测试中都成功地使用了这种方法。

- (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;
}
于 2014-05-21T06:53:37.713 回答
5

自 Xcode 6 以来,它作为一个类别内置到 XCTest 中:

https://stackoverflow.com/a/24705283/88164

于 2014-10-07T17:58:55.900 回答
3

这是另一种选择,XCAsyncTestCase,如果你需要使用它,它可以很好地与 OCMock 配合使用。它基于 GHUnit 的异步测试器,但使用常规的 XCTest 框架。与 Xcode 机器人完全兼容。

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];
}
于 2013-10-23T14:11:15.097 回答
3

Sam Brodkin已经给出了正确的答案

只是为了让答案看起来更好看,我把示例代码带到这里。

使用 XCTestExpectation。

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

- (void)testDocumentOpening
{
    // Create an expectation object.
    // This test only has one, but it's 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];
    }];
}
于 2015-01-30T14:33:29.080 回答
3

AGAsyncTestHelper是一个用于编写具有异步操作的单元测试的 C 宏,可与 SenTestingKit 和 XCTest 一起使用。

简单明了

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

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

    WAIT_WHILE(!jobDone, 2.0);
}
于 2013-10-13T04:04:29.617 回答
2

您可以像这样快速使用异步 api 调用

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)")
        }
    }
}
于 2017-10-25T05:12:52.013 回答
1

我建议你看看Facebook-ios-sdk 的测试。这是如何在 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");
}
于 2013-10-13T03:59:34.967 回答
0

试试 KIWI 框架。它功能强大,可以帮助您进行其他类型的测试。

于 2014-10-07T20:58:25.670 回答
0

I recommend you connection semaphore + runloop, i also wrote method which take block:

// 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)

method:

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

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

and call

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

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

});
于 2014-03-20T15:54:04.733 回答
0

如果您使用的是 XCode 6,您可以像这样测试异步网络调用:

Xcode 6 中的 XCTest 和异步测试

于 2014-10-14T19:02:41.533 回答