こんにちは、QAチームのアベです。
iOS SDK でのコーディングって楽しいですよね。でも完成度の高いアプリを目指すと開発ライフサイクルの中でいくつものテストを充分に行う必要があり大変なことも多いです。難しいのは「充分に」というところです。何をもって「充分」といえるでしょうか?時間とお金があればいくらでもテストするよ!と思うものですが、時間とお金が潤沢にあるプロジェクトは現実にありません。
そこでコストと納期とのトレードオフで重要なところを重点的にテストするという戦略があります。 masuidrive さんも言ってました、「モデルの UnitTest は書く、Integration はあきらめよう」と。戦略は様々ですが比較的テストコードを書き易いモデルレイヤのプロダクトコードに対するユニットテストを充分に行うというのが現実的のようです。
本記事ではモデルレイヤにあるようなコードのユニットテストの充分さ(カッコ良く「充当性」と呼びます)を測る方法を考えてみます。必要なのは SenTestingKit と gcov す。前者は Xcode に標準で含まれているので説明はいらないでしょう。 gcov は gcc のコードカバレッジツールです。こちらも標準で入っているのですぐ使用できます。
まず gcov を使ったコードカバレッジの流れを簡単に説明します。詳細を知りたい方はTerminal.app で man してみてください。とりあえず Makefile に gcov 用のオプションを記します。 comp ターゲットの -fprofile-arcs -ftest-coverage というところです。
comp:
gcc -Wall -fprofile-arcs -ftest-coverage -o gcov_test gcov_test.c
clean:
rm -f *.gcno
rm -f *.gcda
rm -f *.gcov
@if [ -f *.info ]; then
rm -f *.info;
fi
make すると gcov_test.gcno というファイルが生成されます。さらに生成された実行ファイル gcov_test を実行すると gcov_test.gcda というファイルが生成されます。ここで gcov を実行します。
> ./gcov_test
> gcov gcov_test.gcda
そうすると gcov_test.c.gcov というファイルが生成され、測定したカバレッジデータの概要が Terminal に出力されます。以下が出力例です。
File 'gcov_test.c'
Lines executed: 80% of 19
gcov_test.c:creating 'gcov_test.c.gcov'
上記から実行されたコードが全体のコード量の 80% であることが分かります。この gcov ファイルに詳細なカバレッジデータが記されています。もうお気づきと思いますがこれはステートメントカバレッジの値です。コードカバレッジを測定するときは判定の厳しいブランチカバレッジを採用することはご存知ですね。なお、ブランチカバレッジも測定するとき時は -b オプションを指定します。
> gcov -b gcov_test.gcda
gcov がどのようなものか分かったでしょうか? ここからは Xcode での測定となります。Xcode4.2、iOS SDK5.0 環境でユニットテストからプロダクトコードを実行してコードカバレッジを測定してみます。その前に Xcode でちょっとした準備が必要です。
まずプロジェクトターゲットに共有ライブラリを追加します。SummaryのLinked Frameworks and Libraries で libprofile_rt.dylib というライブラリを追加します。/Develper/usr/lib/ 配下にあります。
次に Other Linker Flags の Debugに-lgov を指定します。
さらに Other C Flags の Debugに-fprofile-arcs -ftest-coverage を指定します。Makefile の中で指定したものと同じですね
そして Generate Test Coverage Files と Instrument Program Flow の Debug を Yes とします。
Xcode の準備はこれで完了です。ここで測定対象のプロダクトコードを簡単に説明しておきます。
@implementation LSSTypeOfCompiled
(id)initWithFileExtension:(NSString )extension
{
self = [super init];
notAvailablePatternsOfCompiled = [NSArray arrayWithObjects:@"[/*]+[w|W]", … , nil];
return self;
}
(BOOL)isAvailable:(NSString *)aLine
{
NSError *error = NULL;
id notAvailablePattern;
for (notAvailablePattern in notAvailablePatternsOfCompiled)
{
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:(NSString *)notAvailablePattern
options:NSRegularExpressionCaseInsensitive error:&error];
NSUInteger numberOfMatches = [regex numberOfMatchesInString:aLine options:0 range:NSMakeRange(0, [aLine length])];
if (numberOfMatches > 0)
{
return FALSE;
}
else
{
return TRUE;
}
}
return TRUE;
}
@end
あるソースコードファイルの有効ラインをチェックするためのメソッドが isAvailable です。このメソッドにソースコードのラインを1行づつ渡してそれが無効か有効かを正規表現で判定しています。無効なラインは改行のみ、コメント行、ブロックステートメントのみでそれ以外は有効と定義します。ユニットテストのテストコードは以下の通りです。
- (void)setUp
{
typeOfCompiled = [[LSSTypeOfCompiled alloc] initWithFileExtension:@"m"];
}
(void)testLineBeginsWithCommentsIncludingAstBegins;
{
NSString compareLine = @"/";
STAssertFalse([typeOfCompiled isAvailable:compareLine], @"test failed.");
}
(void)testLineBeginsWithTabsAndCommentsIncludingAstBegins;
{
NSString compareLine = @"tt/";
STAssertFalse([typeOfCompiled isAvailable:compareLine], @"test failed.");
}
ラインがコメント開始要素のみ(この場合 /*)と、タブから始まりその後にコメント開始要素がある場合に isAvailable メソッドが正しく無効ラインと判断するかをテストする2つのメソッドを実装したテストコードです。
上記のテストコードから成るユニットテストを実行します。そうするとプロダクトコードの gcno, gcda ファイルが生成されているはずです。ディレクトリは /Users/${USER_NAME}/Developer/Xcode/DeriverdData/${PROJECT_NAME}/Build/Intermediates/${PROJECT_NAME}.build/Debug-iphonesimulator/{PROJECT_NAME}.build/Objects-normal/ です(深いですね)。それから gcov 実行します。
> gcov -b ./LSSTypeOfCompiled.gcda
プロダクトコードのgcov ファイルが生成されて、Terminal.app には以下のようにカバレッジデータの概要が表示されています。
File '/Users/Hoge/Projects/BlogArticle/BlogArticle/Data/LSSTypeOfCompiled.m'
Lines executed:84.21% of 19
Branches executed:60.00% of 10
Taken at least once:30.00% of 10
No calls
/Users/Hoge/Projects/BlogArticle/BlogArticle/Data/LSSTypeOfCompiled.m:creating 'LSSTypeOfCompiled.m.gcov'
生成された LSSTypeOfComplied.m.gcov ファイルの中を見てみます。
-: 0:Source:/Users/Hoge/Projects/BlogArticle/BlogArticle/Data/LSSTypeOfCompiled.m
-: 0:Graph:LSSTypeOfCompiled.gcno
-: 0:Data:LSSTypeOfCompiled.gcda
-: 0:Runs:0
-: 0:Programs:0
(省略)
-: 9:#import "LSSTypeOfCompiled.h"
-: 10:
#####: 11:@implementation LSSTypeOfCompiled
-: 12:
2: 13:- (id)initWithFileExtension:(NSString *)extension
-: 14:{
2: 15: self = [super init];
(省略)
-: 33:
2: 34: return self;
2: 35:}
-: 36:
2: 37:- (BOOL)isAvailable:(NSString *)aLine
-: 38:{
2: 39: NSError *error = NULL;
-: 40:
2: 41: id notAvailablePattern;
8: 42: for (notAvailablePattern in notAvailablePatternsOfCompiled)
-: 43: {
2: 44: NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:(NSString *)notAvailablePattern
2: 45: options:NSRegularExpressionCaseInsensitive error:&error];
2: 46: NSUInteger numberOfMatches = [regex numberOfMatchesInString:aLine options:0 range:NSMakeRange(0, [aLine length])];
2: 47: if (numberOfMatches > 0)
-: 48: {
2: 49: return FALSE;
-: 50: }
-: 51: else
-: 52: {
#####: 53: return TRUE;
-: 54: }
4: 55: }
#####: 56: return TRUE;
2: 57:}
-: 58:
-: 59:@end
49行目の FALSE を返すところは2回実行されています(2つのテストメソッドが実行している)が、53, 56行目は実行されていないことが分かります。つまりテストされていないわけです。なので有効行と判定するようなテストメソッドを追加します。そして上記同様にユニットテスト、gcov も実行します。結果、ステートメントカバレッジ値が5%ちょっと向上しました。gcov ファイルの当該メソッド部分の結果のみを記します。最初のテストでは53行目を実行してなかったのですが、今回は追加実行したテストメソッド testLineWithSemanticsStatement により1回実行されていることが分かります。
- (void)testLineWithSematicsStatement
{
NSString *compareLine = @"static bool loadImage(ofPixels_ & pix, string fileName){";
STAssertTrue([typeOfCompiled isAvailable:compareLine], @"test failed.");
}
> gcov -b LSSTypeOfCompiled.gcda
File '/Users/Hoge/Projects/BlogArticle/BlogArticle/Data/LSSTypeOfCompiled.m'
Lines executed:89.47% of 19
Branches executed:60.00% of 10
Taken at least once:40.00% of 10
No calls
/Users/Hoge/Projects/BlogArticle/BlogArticle/Data/LSSTypeOfCompiled.m:creating 'LSSTypeOfCompiled.m.gcov'
-: 48: {
2: 49: return FALSE;
-: 50: }
-: 51: else
-: 52: {
1: 53: return TRUE;
-: 54: }
6: 55: }
条件分岐がもっと複雑であればブランチカバレッジの測定も非常に有効です。本当であればブランチカバレッジでの測定を採用すべきです。今回は話を分かり易くするためにステートメントカバレッジの測定例を取り上げています。
カバレッジが低ければテストコードを追加して未実行のラインを極力減らすことでバグの潜在を予防することが可能になります。コードカバレッジの値からユニットレベルでのテストの充当性を定量的に把握することできます。このようなプロセスを継続的に行い数値データを蓄積することでクラスの責務に応じた適正カバレッジ値を見出すことができます。このような定量管理は「テストが充分か?」といった問いに数値傾向に基づいて判断できるメリットがありますが、数値のみに注目してその値を盲目的に過信してしまうことがないように注意が必要です。
ちなみに、gcov ファイルの中身をもっとグラフィカルに HTML ファイルで表示してくれるツールがあります。それが lcov です。標準では入っていないのでサイトから tarball をダウンロードしてコンパイルしてください。MacPorts からもインストールできます。使い方は以下の通りです。
lcov コマンドに出力先のディレクトリと出力ファイル名(拡張子は info とする)を指定して HTML ファイル生成の準備をします。
> lcov -directory ./ --capture --output BLOG_ARTICLE.info
それから genhtml で HTML ファイルを生成します。生成された HTML ファイルは gcov ファイルよりもはるかに見易いはずです。出力例はlcov のサイトで見れるので参照してみてください。
> genhtml -o ./ BLOG_ARTICLE.info
gcov、lcov、genhtml を順次実行する手間が必要ですがシェルスクリプトを作成しておけばさほど面倒ではないと思います。もっと楽をしたい方には coverstory というMac用のアプリケーションが便利です。ユニットテストを実行して gcda ファイルがある場所をアプケーション上から指定するだけで lcov に劣らないレポーティングをしてくれます。以下がそのUIのスクリーンンショットです。
このようなユニットテストツールとカバレッジツールの組み合わせは Ecplise 環境ではいくつかありますが、Xcode と Unix コマンドラインツールとでも立派で実用的なツールスイートとなります。クオリティの高い魅力的なアプリを世に出すための手段のひとつとしてユニットテストの充当性を調べてはどうでしょうか?
[ 参照サイト ]
iOS Developer Library - Unit Testing Applications
Using Coverstory - How to use CoverStory (Google Project Hosting)
Mac OS X Library - Usign GCOV from Xcode
Mac OS X Library - gcov(1) Mac OS X Developer Tools Manual Page