A Day In The Life

とあるプログラマの備忘録

SenTestCase で非同期処理のテストをする方法

今年の2月ごろから iOS 用の HTTP 通信ライブラリの開発をしていています。

非同期で HTTP 通信をするだけの簡単なライブラリなんですが、ユニットテスト(Unit Test)をする時に少しコツが必要だったのでその紹介です。ここで紹介する方法は非同期(Async)処理のテスト全般で使える技だと思います。
iOSユニットテストを実施するときは iOS に標準で付属している SenTestingKit というフレームワークを使います。

ユニットテストの書き方

SenTestingKit の使い方は JavaJUnit とほとんど同じで簡単です。SenTestingKit では SenTestCase というユニットテストのための基底クラスが提供されています。まず SenTestCase クラスを継承したテスト用のクラスを作成します。次にテスト用のクラスに setUp メソッドと tearDown メソッドを追加してそれぞれテストの前準備処理と片付け処理を定義します。test で始まるメソッドに実際のテストを書いていきます。

@interface R9HTTPRequestTests : SenTestCase

@end

@implementation R9HTTPRequestTests
- (void)setUp
{
  [super setUp];
  /*
   * テストの前準備処理
   */
}
- (void)tearDown
{
  /*
   * テストの後片付け処理
   */
  [super tearDown];
}
- (void)testHoge1
{
  // assert を使ったテストを書く
}
- (void)testHoge2
{
  // テスト
}
@end

ここまでがマルチスレッドじゃない普通のテストを実施するときの方法です。

マルチスレッド処理のテストを実施する

非同期処理のテストをする場合、普通にテストコードを書くと非同期の処理が終わる前に tearDown メソッドが呼ばれてテストが終了してしまいます。そうならないためにテスト終了用のフラグを用意して tearDown メソッドで非同期処理が終わるまでループを使って待機させます。こんな感じです。

@implementation R9HTTPRequestTests {
  // テスト終了用フラグ
  BOOL _isFinished;
}

- (void)setUp
{
  [super setUp];
  // フラグをオフ
  _isFinished = NO;
}

- (void)tearDown
{
  // テストが終了するまで待機
  do {
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
  } while (!_isFinished);
  [super tearDown];
}
@end

あとは普通にテストコード書いてテストを終了させたいタイミングでフラグをオンにします。
こんな感じです。例は R9HTTPRequest のテストコードの抜粋です。

- (void)testGETRequest
{
  R9HTTPRequest *request = [[R9HTTPRequest alloc] initWithURL:[NSURL URLWithString:@"http://www.apple.com"]];
  [request setFailedHandler:^(NSError *error){
    NSLog(@"%@", error);
    // テスト失敗!
    STFail(@"Fail");
    // テスト終了
    _isFinished = YES;
  }];
  [request setCompletionHandler:^(NSHTTPURLResponse *responseHeader, NSString *responseString){
    NSLog(@"%@", responseString);
    // メインスレッドで実行されていることを確認
    STAssertTrue([[NSThread currentThread] isMainThread] == YES, @"");
    // ステータスコード200が返ってくることを確認
    STAssertTrue(responseHeader.statusCode == 200, @"");
    // テスト終了
    _isFinished = YES;
  }];
  [request startRequest];
}

NSRunLoop を使わないとメインスレッドがロックされちゃいます

コツとしてはループでテストの終了をまつのに NSRunLoop を使っているところです。実行ループを使わずに以下のような手抜きコードを書くと、スレッドを新しく作って実行したあとメインスレッド(UI Thread)に処理を戻す時にうまくテストができませんでした。

- (void)tearDown
{
  // ダメな例。メインスレッドがロックされちゃいます!
  do {} while (!_isFinished);
  [super tearDown];
}

上記の手抜きコードで実際にうまくいかなかったテストの例です。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
  // 新規スレッドで実行されるコード
  // ここは実行される!
  NSOperationQueue *queue = [NSOperationQueue mainQueue];
  [queue addOperationWithBlock:^{
    // メインスレッドで実行されるコード
    // いつまでたっても実行されない!
    STAssertTrue([[NSThread currentThread] isMainThread] == YES, @"");
    _isFinished = YES;
  }];
}];

メインスレッド→新規スレッド→メインスレッドのときに単純なループ処理だとメインスレッドがロックされてしまうため処理が走りません。NSRunLoop を使ってやるとメインスレッドがロックされることなくテストができますよというよくよく考えたら当たり前の話です。が、ついつい忘れてしまいがちなので気をつけましょう。

関連記事

2012年7月1日の修正でブロックの処理をすべて UI スレッドで実行するように仕様を変更しました。