CocoaのUnitTest

これまでの職歴では大規模開発中心で大抵は発注元の大手ベンダーがチームを作ってテストをすることが多くて単体テストってあまり身近なものじゃなかったんだけれど、今は大規模開発なんて無縁だし、僕が書いたコードをデバグしてくれるのは僕一人という状況だから開発サイクルにうまくテストを組み込むのは必須事項なんです。それで、Cocoaはまだ趣味のレベルなんだけどいろんなテストフレームワークに触れるのも今後大事だと思ったので勉強してみました。

CocoaのテストフレームワークはSenTestingKit.Frameworksが用意されていてこれを使います。具体的にはプロジェクトにUnit Test Bundleを組み込むとビルドフェーズでテストが自動化されます。また、ドライバ作成用にはSenTestCaseというクラスが用意されているのでクラスfooをテストするにはこのSenTestCase(を継承した)クラスからfooのメソッドを呼び出してテストを実行することになります。

自作したpasswdCacheというアプリケーションで使っている暗号化/複合化のメソッドのテストを実際にやってみました。

まず、プロジェクトにテスト用ターゲット(もちろんUnit Test Bundle)を追加しターゲットインスペクタの「一般」タブから「+」ボタンで依存するアプリケーションを追加します。通常はテストしたいクラスを利用するアプリケーションになります。次に「ビルド」タブの検索フィールドにbundleをタイプします。いくつかのbundle関連項目が表示されますがこのうち「バンドルローダー」と「テストホスト」を設定します。

テストコードではテスト対象となるクラスのメソッドや変数を定義しませんがこれらはこのバンドルローダーで指定したアプリケーションから動的に参照されます。これによって、テストモジュールのためにわざわざテスト対象となるクラスをコンパイルする必要はなくなるわけです。

バンドルセッティング

実際の指定は変数を使って$(BUILT_PRODUCTS_DIR)/passwdCache.app/Contents/MacOS/passwdCacheをバンドルローダーに、テストホストにはバンドルローダーそのものを指定するので$(BUNDLE_LOADER)を指定します。これで準備は完了。次はテストドライバを作成します。

「新規ファイル...」から「Objective-C test case class」を選択し適当な名前をつけてテストターゲットへ追加します。SenTestCaseクラスを継承したテストドライバが生成されます。

#import <SenTestingKit/SenTestingKit.h>

@interface PBUtilitiesTest : SenTestCase {
 NSArray* testData;
}
@end

後は、テストケースを実装するだけ。テストケースの実装は、

  • testで始まるメソッドに実装する
  • パラメータはとらない
  • 戻り値はなし(void)

のルールで書けば後は何でもOK。ただし、テストの準備と後始末メソッドのsetUp/tearDownは例外となります。

- (void)setUp
{
  testData = [NSArray arrayWithObjects:@"mypassword",@"yourpassword",nil];
}
- (void)testEncDec
{
  id indata;
  NSEnumerator* enumerator = [testData objectEnumerator];
  while((indata=[enumerator nextObject]) != nil)
    {
      NSData* temp = [PBUtilities encBlowfish:indata];
      NSString* odata = [PBUtilities decBlowfish:temp];
      STAssertEqualObjects(indata,odata,@"odata shuld be equal indata");
    }
}
- (void)testMustNil
{
  STAssertNil([PBUtilities encBlowfish:@""],@"msut be return nil");
}
- (void)testDecWithPassPhrase
{
  NSData* temp = [PBUtilities encBlowfish:[testData objectAtIndex:0]];
  NSString* odata = [PBUtilities decBlowfishWithPhrase:temp phrase:@"wrong_phrase"];
  STAssertNotNil(odata,@"must be empty string,NOT nil");
  STAssertEqualObjects(@"",odata,@"must be return empty string");
}

ここでは暗号化したデータを複合して正しく元データを復元できるかテストしています。STAssertXXXXはSenTestCaseクラスで使えるマクロで、他にも

STAssertNil(a1, description, ...)
a1がnilであることをテストする
STAssertNotNil(a1, description, ...)
a1がnilでないことをテストする
STAssertTrue(expression, description, ...)
expressionがtrueであることをテストする
STAssertFalse(expression, description, ...)
expressionがfalseであることをテストする
STAssertEqualObjects(a1, a2, description, ...)
a1オブジェクトとa2オブジェクトが等しいことをテストする
STAssertEquals(a1, a2, description, ...)
a1とa2が等しいことをテストする
STAssertEqualsWithAccuracy(left, right, accuracy, description, ...)
leftとrightが指定した精度で一致するかテストする
STAssertThrows(expression, description, ...)
expressionが例外をスローすることをテストする
STAssertThrowsSpecific(expression, specificException, description, ...)
expressionがspecificExceptionをスローすることをテストする
STAssertThrowsSpecificNamed(expr, specificException, aName, description, ...)
exprがspecificExceptionのaNmaeをスローすることをテストする
STAssertNoThrow(expression, description, ...)
expressionが例外をスローしないことをテストする
STAssertNoThrowSpecific(expression, specificException, description, ...)
expressionがspecifixExceptionをスローしないことをテストする
STAssertNoThrowSpecificNamed(expr, specificException, aName, description, ...)
exprがspecificExceptionのaNameをスローしないことをテストする
STAssertTrueNoThrow(expression, description, ...)
expressionがtrueで例外もスローしないことをテストする
STAssertFalseNoThrow(expression, description, ...)
expressionがfalseで例外もスローしないことをテストする
STFail(description, ...)
常にテスト不合格

など多くのマクロが用意されています。もちろん、自前でAssertを書いてもOKでその場合は最後のマクロSTFailとif文などで処理すると良いと思います。

通しでやってみてターゲットのバンドルセッティングのところがCocoa特異の流儀になるのでちょっとわかりずらかったです。最初はせっせとテストターゲットに対象クラスのコンパイルを追加していました。ビルドフェーズの最後の部分で${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTestsを実行して実際のテストをするところはmake testみたいなものと考えればまあそんなに違和感はなかったですね。

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/283

返信