iOSで非同期呼び出しの単体テストを行っているときに問題が発生しました。(ビューコントローラーでは正常に動作していますが。)
誰かが以前にこの問題に直面したことがありますか?待機機能を使用してみましたが、同じ問題が発生します。
これを行うための良い方法の例を提案してください。
iOSで非同期呼び出しの単体テストを行っているときに問題が発生しました。(ビューコントローラーでは正常に動作していますが。)
誰かが以前にこの問題に直面したことがありますか?待機機能を使用してみましたが、同じ問題が発生します。
これを行うための良い方法の例を提案してください。
コールバックが呼び出されるまで、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);
これは、非同期テストのネイティブサポートに関するAppleの説明です。
TL; DRマニュアル:
見るXCTextCase+AsynchronousTesting.h
XCTestExpectation
パブリックメソッドが1つしかない特別なクラスがあります。- (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);
}
}];
}
この投稿で提案されている解決策の多くには、非同期操作が完了しない場合、「完了」フラグが設定されず、テストが永久にハングするという問題があると思います。
私は多くのテストでこのアプローチをうまく使用しました。
- (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;
}
Xcode 6以降、これはカテゴリとしてXCTestに組み込まれています。
AGAsyncTestHelperは、非同期操作で単体テストを作成するためのCマクロであり、SenTestingKitとXCTestの両方で機能します。
シンプルで要点
- (void)testAsyncBlockCallback
{
__block BOOL jobDone = NO;
[Manager doSomeOperationOnDone:^(id data) {
jobDone = YES;
}];
WAIT_WHILE(!jobDone, 2.0);
}
別の代替手段である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];
}
答えを一目で見やすくするために、ここにサンプルコードを示します。
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];
}];
}
このように迅速に非同期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)")
}
}
}
Facebook-ios-sdkのテストを確認することをお勧めします。これはiOSで非同期単体テストをテストする方法の良い例ですが、個人的には非同期テストは同期テストに分割する必要があると思います。
FBTestBlocker:現在のスレッドが指定されたタイムアウトで終了するのを防ぐブロッカー。これをプロジェクトにドラッグアンドドロップできますが、プロジェクトにOCMock関連のものがない場合は、削除する必要があります。
FBURLConnectionTests:あなたが見るべきテスト例。
このコードスニペットはあなたにいくつかのアイデアを与えるはずです
- (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");
}
セマフォとランループを接続することをお勧めします。ブロックを取るメソッドも作成しました。
// 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();
}];
});
KIWIフレームワークを試してください。これは強力で、他の種類のテストに役立つ可能性があります。
XCode 6を使用している場合は、次のように非同期ネットワーク呼び出しをテストできます。