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 のチェックは外しておいて下さい。
GHUnit 導入
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
削除が終わったら、以下のファイルを GHCountTests ターゲットに追加します。
github からチェックアウトしたソースにあります。
gh-unit/Tests/GHUnitIOSTestMain.m
追加できましたか?次で最後の作業になります。
GHCountTests ターゲットの Build Settings の Other Linker Flags の値に "-ObjC -all_load" を加えます。
これで完了です。ビルドしてみて下さい、テスト専用のビューが表示されたなら成功です!やりましたね!
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 を指定してほしいのですが、ファイルパスの関係もあり実ファイルの配置場所はプロジェクト直下にしてください。
追加後は以下のようになると思います。
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."); }