2012年1月24日火曜日

Jenkins を iOS アプリ開発に導入してみた (gcov編)


前回 はSenTestKitに続いてGHUnitを導入して、Jenkins上でGHUnitによるテストの自動実行を行いました。今回はさらにステップアップして、gcovを使用してコードカバレッジを取るようにしてみようかと思います。


■gcovを使ったコードカバレッジの取得(Xcode 4.3版)

Xcode 4.3の場合は以下の参考ページで紹介されている方法がオススメです。実機ででもシミュレータでも動作してお得です。
http://www.cocoabuilder.com/archive/xcode/314794-xcode-4-3-moved-libprofile-rt-how-to-reference-it-now.html
http://www.infinite-loop.dk/blog/2012/02/code-coverage-and-fopen-unix2003-problems/
https://github.com/InfiniteLoopDK/ILTesting
  1. Generate Test Coverrage FilesとInstrument Program FlowにYESを指定する
  2. http://www.infinite-loop.dk/blog/2012/02/code-coverage-and-fopen-unix2003-problems/で紹介されているILProfilerCompat.cを組み込む(これによってfopen$UNIX2003によるエラーを防ぐ)
これだけで大丈夫です。


■gcovを使ったコードカバレッジの取得(Xcode 4.2版)

【2012/03/15追記】こちらの方法はXcode 4.2が前提となっており、Xcode 4.3以降は動作しません!

gcovとはgccに付属されているC言語用のカバレッジ測定ツールです。これを使うことで、C言語と、その拡張であるObjective-Cのコードカバレッジをバッチリ取得することができます。ということで早速プロジェクトに組み込んでみましょう。幸いにしてすでに先駆者の方がいらっしゃいました。
http://tech.naver.jp/blog/?p=706
基本的に上記に記載されている通りの手順で導入すれば簡単にgcovによるコードカバレッジの取得ができますが、実際にやってみたところ幾つか補足があるので追記します。

先ほど紹介した記事では/Developer/usr/lib/libprofile_rt.dylibの追加と-lgcovの指定両方を行なっているのですが、私が試したところどちらか片方だけで成功するということがわかりました。具体的には以下のとおりになります。

1. /Developer/usr/lib/libprofile_rt.dylibを使うパターン
2. -lgcovを使うパターン

それぞれ見ていきます。

1. /Developer/usr/lib/libprofile_rt.dylibを使うパターン

/Developer/usr/lib/libprofile_rt.dylibをプロジェクトに追加し、Other C Flagsの-fprofile-arcs -ftest-coverageを設定すればOKです。それ以外の手順は必要ありません。
このパターンですとスムーズにgcovの結果を出力することができましたが、残念ながらこの方法はSimulator上でしか使用できません。というのも、/Developer/usr/lib/libprofile_rt.dylibがそもそもarmv6, armv7用のバイナリを含んでいないという問題があるのと、ライブラリ探索パス"/Developer"以下がDeviceビルド時に勝手にiphoneos.sdk以下の/Developerに置換されてしまうため、ビルド時にlibprofile_rt.dylibをリンカが発見できずエラーになる問題があるためです。基本的にSenTestingKitのようなSimulatorでしか実行しないビルドターゲットでやるのが良いと思われます。

2. -lgcovを使うパターン

参考: http://stackoverflow.com/questions/5101014/code-coverage-not-showing-results-using-xcode-gcov/5140459#5140459
-lgcovをOther Linker Flagsに追加し、Generate Test Coverrage FilesとInstrument Program FlowにYESを指定すればOKです。それ以外の手順は必要ありません。
このパターンですとlibgcovはarmv6, armv7でも動作するので実機でのカバレッジ取得ができるのですが、問題はそのままの設定でビルドして実行すると実行時にクラッシュします。これはlibgcovが実行時にカバレッジデータを書きだす際に、デフォルトの設定ではiOSのサンドボックスの外にアクセスしようとするため書き出しに失敗してしまうのが原因です。したがって上記参考URLに従ってGCOV_PREFIX環境変数を使いサンドボックス内部にカバレッジデータを書きだすように指定してやる必要があります。
この方法は環境変数を実行時に設定する必要があって面倒なのと、カバレッジデータが書き出される箇所がJenkinsのビルドディレクトリの中ではなくiOSシミュレータまたは実機のサンドボックスの中になってしまうため、Jenkinsからカバレッジの取得を行うのが困難になってしまう問題があります。

今回は1. のパターンを採用し、SenTestingKit上でSimulatorビルド時のカバレッジ取得だけを行うことにしました。GHUnitでも実機上での動作を諦めれば問題ないのですが・・・まだ課題が多い感じですね。

ビルド設定ができたらビルドして、きちんとgcovコマンド経由でカバレッジが取得できていることを確認しましょう。


■gcovrを使ってCoberturaのXML形式に変換する

gcovのカバレッジデータの取得ができるようになったら、今度はJenkinsと連携させるために、gcovの出力をCoberturaというJava環境のカバレッジ計測ツールの出力するXML形式に変換する必要があります。これはJenkinsがgcovのカバレッジデータ出力の集計に対応していない為です。っていうかgcovの出力は単なるタブで区切られた文字列で人間以外にはとても優しくないので集計ができないのです。
ここで、Python製のgcovrというツールをJenkinsサーバマシンに導入します。 gcovrをgcovの代わりに使うことで、CoberturaのXML形式で結果を出力できるようになるほか、カバレッジ計測対象となるファイルを簡単にフィルタリングできる(たとえばテストケースクラスのカバレッジなんか集計してもしょうがないので除外するなどできる)のが大きな魅力です。
http://wiki.hudson-ci.org/pages/viewpage.action?pageId=45482230
https://software.sandia.gov/trac/fast/wiki/gcovr

インストールはMacマシンであれば非常に簡単で、以下のコマンドを打つだけです。
sudo easy_install gcovr
インストールが完了したら、あとは
gcovr --xml --output=cobertura-report.xml build/
とかやれば一発でbuildディレクトリ配下のすべてのカバレッジを計測結果を集計してXML形式で書きだしてくれます。
ここでもし、テストケースのカバレッジは除外したいなどという要件がある場合は、
gcovr --xml --exclude=".*/.*Test(s)?\.[(c)(cpp)(m)(mm)]" --output="cobertura-report.xml" build/
などとすればOKです。


■JenkinsのCoberturaプラグインを使って結果を集約する

CoberturaのXML形式で結果が取れるようになってしまえばあとは楽勝です。JenkinsにCoberturaプラグインをサクっとインストールして、
https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin
あとは以下のようにJenkinsのジョブ設定を書いてしまうだけです。先述の通り、SenTestingKit上でSimulatorビルド時のカバレッジ取得だけを行いたいので、既存のSenTestingKit用単体テストジョブのXcodeビルドの後でgcovrを実行するシェルスクリプトを実行してやれば一発です。


カバレッジが取れるようになると断然見栄えがしますね。

2012年1月23日月曜日

Jenkins を iOS アプリ開発に導入してみた (GHUnit編)


前回 はSenTestKitを用いてJenkins上で単体テストの自動実行を行いました。今回はGHUnitを使った単体テストの自動実行にチャレンジしてみたいと思います。またついでといっては何ですが、単体テスト時に必要になってくるモックを作成するためのライブラリOCMockも同時に導入してみようと思います。


■なぜGHUnitを使うのか

GHUnitを使うことで、SenTestingKitと比べて以下のようなメリットが得られます。
  • 非同期処理のテストを行うための仕組みが用意されている(GHAsyncTestCase)これをSenTestingKitないし他のテスティングフレームワークでやろうとすると大変骨が折れます。
  • .app形式(要するに実際のiOSアプリケーション)でテストを実行するため、UIApplicationやUIWindowといったUIコンポーネントを使うクラスのテストが可能になる。UIテストの支援をするための仕組みも最近のGHUnitには追加されている。
    • SenTestingKitの"Logic Test"では想定通りに動作しないUIKitやFoundationのクラスが幾つかあり、中には純粋なロジックで使いそうなクラスも含まれている。そのためSenTestingKitではテストできないロジッククラスが発生する場合があるので、そういう時はGHUnitを使うと良い。
    • 参考: https://gist.github.com/1662887
  • 実機上でテストを実行できる。そのため実機でのみ発生するバグをつかまえることが可能。
  • SenTestingKitのテストケースをそのまま実行できるため上位互換として使用できる。
逆にGHUnitにはSenTestingKitと比べて以下のようなデメリットがあります。
  • Xcode 4で統合されたUnitTestingの恩恵に預かれない。すなわちCmd+U一発でテストケースを実行することができない。Xcode経由だとCmd+Rでアプリケーションとして起動して、その後起動したアプリのテスト実行ボタンを押す手間が必要になるため、開発のテンポが乱されてしまう。特にTDDを採用している場合には深刻。
  • 上記問題を回避するためCUI経由でテストを実行することもできるのだが、Xcodeのエディタと連携しないため、どこがエラーになっているのかが一目でわかりづらい。
  • カバレッジの取得が楽。SenTestingKitをシミュレータ上で実行している時が一番やりやすい。これについては別の記事で解説します。
これを踏まえると、以下のように使い分けができるようになります。
  • SenTestingKitは純粋なロジックの単体テストを実行したいときに良い。Xcodeからすぐに実行できて一瞬で結果がコード上に出るので、開発のテンポを乱さない。TDDに向いている。
  • GHUnitは非同期なAPIを持ったクラスの単体テストを実行したいときに良い。またUIApplication, UIWindowといったUIコンポーネントと結合したり、通信やファイルアクセスなどの外部リソースとの結合をおこなった状態でのテストを作る際にも向いている(純粋な単体テストともユーザー受け入れテストとも異なるため、仮に結合テストと呼ぶことにする)ので、そういったものが必要な場合には最高の選択肢となる。
プロジェクトの開発方針や開発対象によってどのようなテストが必要になるのかが異なってくると思いますが、どれか1つだけ必要であればGHUnitを選択し、きちんとしたXPでやりたいプロジェクトはSenTestingKitに単体テストをやらせ、ユーザー受け入れテストに KIF などを採用し、その中間を埋める必要があればGHUnitも導入する、というのが良いのではないかと考えています。ちょっと面倒ですけど。


■GHUnitを導入する

以下のリポジトリからコードを取得して、XcodeからビルドすればOKです。
https://github.com/gabriel/gh-unit
注意点として、GHUnitは.framework版がそのままgithubからダウンロードできるのですが、
https://github.com/gabriel/gh-unit/issues/69
このような問題が報告されており使えないので、githubからソースコードをクローンしてきて、
cd Project-iOS
make
でビルドしてできたGHUnitIOS.frameworkを使うようにしてください。

frameworkをビルドしたら自分のプロジェクトに追加して、GHUnit用のビルドターゲットを作ります。
http://gabriel.github.com/gh-unit/docs/appledoc_include/guide_install_ios_4.html

ビルドターゲットができたら、次は試しにテストケースを追加してみて、問題なくテストが走るか確認。
http://gabriel.github.com/gh-unit/docs/appledoc_include/guide_testing.html

最後にCUI(要するにxcodebuildコマンド経由)でビルドができるようにします。これをやらないとJenkinsと連携できません。
http://gabriel.github.com/gh-unit/docs/appledoc_include/guide_command_line.html


■OCMockを導入する

以下のリポジトリからコードを取得して、XcodeからビルドすればOKです。
https://github.com/erikdoe/ocmock
iOS向けビルドはスタティックライブラリ用のビルドしか用意されていませんので、どうしてもフレームワークが良いとこだわる人は頑張ってください>< そうでなければ非常に簡単です。ビルドしたら成果物をそのままプロジェクトに突っ込めばすぐ使えるようになります。
OCMock自体の使い方にはここでは触れませんが、例えば以下のようなテストケースが作れたりします。

- (void)testOCMock
{
    // Creating a new stub object from class
    {
        id mockedObject = [OCMockObject mockForClass:[NSString class]];
        STAssertThrows([mockedObject length], @"Mocked object raises exception because the fake method is not ready yet.");
        STAssertThrows([mockedObject isKindOfClass:[NSString class]] , @"Mocked object can't even call isKindOfClass: because it's not ready.");
        STAssertFalse([mockedObject class] == [NSString class], @"Mocked object is not a kind of the target class.");

        [[[mockedObject stub] andCall:@selector(mockedLength) onObject:self] length];
        STAssertEquals([mockedObject length], (NSUInteger)100, @"Mocked object returns the fake value.");

        [[[mockedObject stub] andReturn:@"AllYourBaseAreBelongToUs"] lowercaseString];
        STAssertEqualObjects([mockedObject lowercaseString], @"AllYourBaseAreBelongToUs", @"Returns mocked value.");
        STAssertEquals([mockedObject length], (NSUInteger)100, @"Previously mocked methods are still valid.");
    }

    // Method Stubbing for existing object
    {
        NSString *stubTargetObject = @"I am a stub target.";
        STAssertEquals([stubTargetObject length], (NSUInteger)19, @"The original implementation of the length method.");

        id mockedObject = [OCMockObject partialMockForObject:stubTargetObject];
        STAssertTrue([mockedObject isKindOfClass:[NSString class]] , @"Mocked object is a kind of the target class.");
        STAssertEquals([mockedObject length], (NSUInteger)19, @"Mocked object returns the original value.");

        [[[mockedObject stub] andCall:@selector(mockedLength) onObject:self] length];
        STAssertEquals([mockedObject length], (NSUInteger)100, @"Mocked object returns the fake value.");
        STAssertEquals([stubTargetObject length], (NSUInteger)100, @"Stubbed target object also returns the fake value.");
    }
}
- (NSUInteger)mockedLength
{
    return 100;
}


■Jenkinsと連携させる

最後にJenkins側でビルドターゲットを作りましょう。
http://gabriel.github.com/gh-unit/docs/appledoc_include/guide_ci.html

こんな感じの設定になると思います。

あとはいつもどおりジョブを回してみて問題がないか確認すれば完了です。前回ご紹介したようなSenTestKitの単体テストジョブがうまく回っていれば、それをコピーしてきてちょっと設定を変えれば簡単にできるとおもいます。

2012年1月21日土曜日

Objective-C がこの四年間でどれぐらい進化したのか一目でわかるテストケース

Twitterに流したら思ったよりも好評でしたので、ブログにも上げておきます。

こちらがiOS 2地点でのNSURLConnectionクラスを使った非同期通信のテストケース。

こちらがiOS 5でのNSURLConnectionクラスを使った非同期通信のテストケース。

Blocksはやっぱり偉大です。一つしかテストケースがないうちはまだマシなのですが、これが10個とかになると楽さが全く違ってきます。ぜひためしにURLだけ変えて同じテストケースを10個作ってみてください。iOS 5のBlocksを使ったコードはほとんどコピペだけで終わりますが、iOS 2でのdelegateを使ったコードは他にも変更しなければならない点が多数出てくるはずです。

また実際にこのコードを走らせてみると、理由はよくわからないのですがiOS 5で追加されたAPIを使ったコード(Blocks)のほうがそうでないコードよりも毎回2倍程度(0.1秒程度)高速に動作しているみたいです。ちょっと謎ですが、新しいAPIにはパフォーマンス面でのメリットもありそうです。GCDのおかげかな?