jarinosuke blog

about software engineering, mostly about iOS

GHUnitとOCMockでUnit Test効率化

また、テストを書く。

最近 iOS 界隈のテストのベストプラクティスについて調べているのですが、そこで目に留まった文章があるのでまずそれを紹介します。

How comfortable are you on a bike without a helmet? Writing code without tests is like riding a bike without a helmet. You might feel free and indestructible for now, but one day you’ll fall and it’s going to hurt.
http://paulsolt.com/2010/11/iphone-unit-testing-explained-part-1/

ヘルメットを付けずに自転車を漕ぐのはとても快適ですね。でも、ふとしたある日に取り返しの付かないケガをしてしまうかもしれません。
それを防ぐためにヘルメット = テストを書こう、ってことです。
正直テストを書くのはクソ面倒くさいです。でも途中で取り返しのつかないことにならないためにも頑張りましょう。

それでは本題に入ります。
前回は Xcode 付属の SenTestingKit を用いた簡単な Unit Testing の方法を紹介しました。
今回は iOS/Mac App 開発の Testing Framework において一番有名であろう GHUnit をチュートリアル形式で紹介したいと思います。
またそれに伴いテストケースを効率的に作成するためのライブラリ OCMock についても説明します。

GHUnit を導入するメリット、デメリット。

チュートリアルに入る前に GHUnit を SenTestingKit と比較した際のメリットとデメリットを説明します。

メリット
  • GHUnit はオープンソースです。
  • GHUnit は全てのテストを走らせる事も出来るますし、失敗したテストだけってのもできます。それに対してSenTestingKit は全てを行う事しか出来ません。
  • GHUnit は手軽には知らせる事が出来るアプリケーションで、成功・失敗したテストを見やすく実機上で表示してくれます。SenTestingKit は終わるまで待ち、Xcodeデバッグコンソールからみるしかありません。
  • SenTestingKit のテストケースをそのまま実行できるため上位互換として使用できます。
  • SenTestingKit にはない GHAsyncTestCase という非同期処理のテストを行うための仕組みが用意されています。
  • .ipa 形式で実機上でテストを実行できるので、実機でしか発生しないバグをつかまえることが可能です。
デメリット
  • Xcode 4 から統合された UnitTesting の機構(Command + U)を利用する事が出来ません。コマンドラインツールも用意されているみたいですが、一目で分かりづらいようです。

これらを踏まえ、両方の良いところを取るとなると、大体以下のような使い分けになると思います。

  • ロジックを多く含むモデルオブジェクトの Unit Test には SenTestingKit を使い、 Xcode から実行し結果を逐一確認しながら進める。
  • SenTestingKit ではカバーしきれない非同期テストや、実機を用いた3G回線などの外部リソースも考慮した結合テストなどを行う場合には GHUnit を用いる。

(ちなみに Mac 上で 3G回線などネットワークのシミュレートを行うソフトウェアが Developer ツールに付属されていたりもします。)

それでは GHUnit をプロジェクトに導入していきましょう。

プロジェクト作成

今回は前回とは別に新たにプロジェクトを1から作成します。
いつも通り Xcode を起動し、Single View Application を選んで以下のように入力しプロジェクトを GHCount という名前で作成します。
今回はテストターゲットは自分で作成するので Include Unit Tests のチェックは外しておいて下さい。
f:id:jarinosuke0808:20120228211259p:image

GHUnit 導入

テストターゲット追加

ナヴィゲーションからプロジェクトを選択し、左下の Add Target を追加しターゲット名を GHCountTests として追加します。
f:id:jarinosuke0808:20120228211300p:image

GHUnitIOS.framework 追加

https://github.com/gabriel/gh-unit からチェックアウトし、以下のコマンドを実行しビルドします。

git clone https://github.com/gabriel/gh-unit.git
cd gh-unit/Project-iOS
make

make が終了すると Finder で GHUnitIOS.framework が表示されると思うので、それをプロジェクトに追加します。
追加できたら一度テストターゲットを GHCountTests とし、シミュレータなどを指定して問題なくビルドできることを確認して下さい。

GHUnit 構築、ビュー差し替え

GHUnit は独自の window や delegate を持っています。
なので、以下の6つのファイルを削除しましょう。
GHCountTestsAppDelegate.h, GHCountTestsAppDelegate.m, MainWindow.xib, GHCountTestsViewController.h, GHCountTestsViewController.m, TestsViewController.xib, main.m
f:id:jarinosuke0808:20120228211301p:image
削除が終わったら、以下のファイルを GHCountTests ターゲットに追加します。
github からチェックアウトしたソースにあります。

gh-unit/Tests/GHUnitIOSTestMain.m

f:id:jarinosuke0808:20120228211302p:image
追加できましたか?次で最後の作業になります。
GHCountTests ターゲットの Build Settings の Other Linker Flags の値に "-ObjC -all_load" を加えます。
f:id:jarinosuke0808:20120228211303p:image
これで完了です。ビルドしてみて下さい、テスト専用のビューが表示されたなら成功です!やりましたね!

GHUnit のテストケース作成

簡単なテストケースを作成してみます。
SampleTestCase.m という名前で GHCountTests ターゲットにファイルを追加し、以下のようなコードを書いて下さい。

#import <GHUnitIOS/GHUnit.h>
 
@interface SampleLibTest : GHTestCase { }
@end
 
@implementation SampleLibTest
 
- (void)testSimplePass {
     // Another test
}
 
- (void)testSimpleFail {
     GHAssertTrue(NO, nil);
}
 
@end

作成したら再度アプリをビルドし、 iOS Simulator のナヴィゲーションバー右上にある Run ボタンをタップしましょう。
以下のように、先ほど作成したテストケースが一つずつ実行されるはずです。

GHUnit テストケースについてのルール(大体 SenTestingKit とほぼ同じ)

何も考えずにサンプルとして書いたテストケースですが、いくつかルールもあります。

  • テストケースクラスは GHTestCase のサブクラス。(非同期テストは GHAsyncTestCase を使う)
  • テストメソッドは必ず void を返す。
  • テストメソッド名は test から始まる。
  • テストメソッドは引数を取らない。

他にも setUp や tearDown なども SenTestingKit と変わりません。
もう少し詳しく知りたい方は以下を参照して下さい。
GHTestCase Class Reference
これで GHUnit の導入は完了です。

OCMock 導入

では次に OCMock の導入を進めていきましょう。
テストケースにおけるモックオブジェクトの役割やメリットなどについては、ここでは触れません。
僕は以下の記事などを読んでテストケースの作成方法の参考にしました。
モックとスタブの違い
[動画で解説]和田卓人の“テスト駆動開発”講座

チェックアウト

なにわともあれ、まずはチェックアウトです。
erikdoe / ocmock

git clone https://github.com/erikdoe/ocmock.git 

チェックアウトできたら以下のファイルをまるごとプロジェクトに追加します。

/Examples/iPhoneExample/Libraries

追加する時に注意して欲しいのは、もちろんターゲットは GHCountTests を指定してほしいのですが、ファイルパスの関係もあり実ファイルの配置場所はプロジェクト直下にしてください。
追加後は以下のようになると思います。
f:id:jarinosuke0808:20120228211304p:image

Build Settings の更新

2箇所、 GHCountTests の Build Settings を更新する必要があります。

  • Library Search Paths

$(SRCROOT)/Libraries を追加して下さい。
f:id:jarinosuke0808:20120228211305p:image

  • Header Search Paths

$(SRCROOT)/Libraries を追加して下さい。 Recursive にチェックを忘れずに。
f:id:jarinosuke0808:20120228211306p:image
これで OCMock の導入は完了です。やりましたね!
最後に OCMock を使ったテストケースを作成して終わりましょう。

OCMock を使用したテストケース

以下のようなテストケースを追加してみました。
詳しくは触れませんが、モックオブジェクトは複数のモデルオブジェクトが相互に関係し合うテストケースにおいて、単一のメインオブジェクトをテストしたい時に力を発揮すると考えています。

// simple test to ensure building, linking,
// and running test case works in the project
- (void)testOCMockPass {
    id mock = [OCMockObject mockForClass:NSString.class];
    [[[mock stub] andReturn:@"mocktest"] lowercaseString];
   
    NSString *returnValue = [mock lowercaseString];
    GHAssertEqualObjects(@"mocktest", returnValue,
                         @"Should have returned the expected string.");
}

- (void)testOCMockFail {
    id mock = [OCMockObject mockForClass:NSString.class];
    [[[mock stub] andReturn:@"mocktest"] lowercaseString];
   
    NSString *returnValue = [mock lowercaseString];
    GHAssertEqualObjects(@"thisIsTheWrongValueToCheck",
                         returnValue, @"Should have returned the expected string.");
}

f:id:jarinosuke0808:20120228211307p:image

また、テストを書いた。

前回同様、今回のプロジェクトは僕の github にあげてあります。
テスティングフレームワークの選択やモックオブジェクトを使うか使わないかは時と場合によると思いますが、知っておいて損はないでしょう。
次回はいよいよ Jenkins を使って iOS プロジェクトの CI に取り組んでいきたいと思います。