Xcode 4 からはじめる Unit Testing
テストを書く。
最近、色々なプロジェクトで Jenkins をはじめとした継続的インテグレーション(CI)という単語を良く聞くようになりました。
僕自身といえば、iOS 開発では 「Xcode 4 から Unit Test 全面サポートがはじまった!」位しかキャッチアップできていませんでした。
なので開発中にデグレが発生したりすると、モデルから一つずつデバッグを繰り返して、気付いたら2時間経ってるみたいなことも多々ありました。
何してるんだろうみたいな感じになって、流石に危機感を覚えたので一旦手を止めて Xcode 開発に所謂今流行っているテスト手法を組み込もうと思いました。
今回は初歩の初歩ということで、Xcode に付属されている Unit Testing フレームワークである SenTestKit を用いた単体テスト環境の構築を行います。
全てを通して Xcode は 4.3 を使用しています。
プロジェクトの作成
Xcode を起動し、新しいプロジェクトを作成します。
選択するテンプレートは何でも大丈夫ですが、ここではテストが目的なので「Empty Application」を選択しました。
プロジェクト名を「CountTest」とします。
作成時は以下のように、Include Unit Tests にチェックをします。
テスト実行
早速テストを実行してみましょう。
「Build Succeeded」のHUDとともにアプリケーションが走り出すかと思いきや、「Test Failed」と出現し立ち上がらない事が確認できればOKです。
以下がその際のエラーとなります。
ではテストファイルを確認してみましょう。
テストファイル
テストファイル自体は上記で確認した
このクラスのメソッドに初めから定義されている testExample メソッド内で実行されている STFail というマクロがエラーを起こしているのが分かりますね。
ここでいくつか疑問が湧いてくると思います。
- いつ、この testExample は実行されたのか?
- そもそも STFail マクロはどこから来たの?
- setUp と tearDown メソッドは何のため?
- これがテストファイルとしてテストケースはどうやって書けば良いの?
とまぁ色々と僕は思った訳ですが、いったんここではテストは置いておいて、テストをするための簡単なモデルクラスを作成しましょう。
モデルクラス説明
プロジェクト名にもあるように単純なカウンターを作成しました。
Xcode から新しいファイルの名前を Counter として作成し、以下のように編集して下さい。
Counter.h
#import <Foundation/Foundation.h> @interface Counter : NSObject { int _count; } @property(nonatomic, assign)int count; - (void)increment; - (void)decrement; - (void)reset; @end
Counter.m
#import "Counter.h" @implementation Counter @synthesize count = _count; - (id)init { self = [super init]; if (self) { //initialize zero count _count = 0; } return self; } - (void)increment { _count += 1; } - (void)decrement { _count -= 1; } - (void)reset { _count = 0; } @end
テストファイル概要
これからテストクラスにCounterクラスをテストするためのテストケースを書いていく訳ですが、上記で勝手にあげた疑問と共に、ここでテストファイルについて説明します。
- setUp メソッドでテスト対象オブジェクトを生成、 tearDown メソッドで全て解放する。
- 上記両メソッドは各テストメソッド開始終了時にそれぞれ毎回呼ばれる。
- テストメソッド名は全て test から始まる名前でなくてなならない。
- テストメソッド内でのSenTestingKit か提供しているマクロで実行し結果を返す。
以下は直接的には関係ないのですが、僕なりに勝手に考えたテストファイルを実装する上でのルールになります。
- テストファイルはテストオブジェクト毎に用意する。
- テストメソッドもテストオブジェクトの持つメソッドと1対1、もしくはそれ以上になるように用意する。
です。こうすることでテストがしっかり書かれているかどうか簡単に把握できるようになります。
新しくテスト用ファイルを追加する場合でですが、New File を選択後、「Objective-C test case class」からデフォルトで SenTestingKit を include したクラスを作成できます。
ここでは一つしかテスト対象がありませんので、元からあったものを使います。
テストケース作成
それでは準備ができたところで、上記の Counter クラスについてのテストケースを先ほどのテストクラスに準備していきます。
上記で記述した自分ルールをもとに以下のようなテストメソッドを用意しました。
実装は以下になります。
またSTからはじまるSenTestingKitのマクロですが、行っている内容については以下のコードで大方分かると思います。
更に詳しく知りたい方はドキュメントを参照して下さい。
Unit-Test Result Macro Reference
- (void)setUp { [super setUp]; // Set-up code here. _counter = [[Counter alloc] init]; } - (void)tearDown { // Tear-down code here. [_counter release]; [super tearDown]; } - (void)testInit { STAssertNotNil(_counter, @"カウンターが初期化できません。"); STAssertEquals(_counter.count, 0, @"カウンターの初期値が0ではありません"); } - (void)testIncrement { [_counter increment]; STAssertEquals(_counter.count, 1, @"カウンターのインクリメントに失敗しました。"); } - (void)testIncrementResetsToZeroAfterTen { for(int i = 0; i < 10; i++) { [_counter increment]; } STAssertEquals(_counter.count, 10, @"カウンターの値が10になっていません。"); [_counter reset]; STAssertEquals(_counter.count, 0, @"カウンターが10の値から正しくリセットされていません。"); } - (void)testDecrement { [_counter increment]; [_counter decrement]; STAssertEquals(_counter.count, 0, @"カウンターが正しくデクリメントされていません。"); } - (void)testReset { [_counter increment]; [_counter increment]; [_counter increment]; STAssertEquals(_counter.count, 3, @"カウンターが正しく3回インクリメントされていません。"); [_counter reset]; STAssertEquals(_counter.count, 0, @"カウンターが正しくリセットされていません。"); }
上記の実装が完了したら再び、Test を実行してみましょう。
問題なく「Test Succeeded」が表示されれば、この Counter クラスの動作は保証された事になります。やりましたね!