jarinosuke blog

about software engineering, mostly about iOS

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 にチェックをします。
f:id:jarinosuke0808:20120219145848p:image:w360

テストターゲットの確認

プロジェクトが作成されたら、Test用のターゲットとファイルが作成されている事が確認できます。
f:id:jarinosuke0808:20120219145849p:image:w360

テスト実行

早速テストを実行してみましょう。
「Build Succeeded」のHUDとともにアプリケーションが走り出すかと思いきや、「Test Failed」と出現し立ち上がらない事が確認できればOKです。
以下がその際のエラーとなります。
f:id:jarinosuke0808:20120219145852p:image
ではテストファイルを確認してみましょう。

テストファイル

テストファイル自体は上記で確認したTestsフォルダに配置されているTests.hとTests.mファイルになります。
このクラスのメソッドに初めから定義されている 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 したクラスを作成できます。
ここでは一つしかテスト対象がありませんので、元からあったものを使います。
f:id:jarinosuke0808:20120219145850p:image

テストケース作成

それでは準備ができたところで、上記の Counter クラスについてのテストケースを先ほどのテストクラスに準備していきます。
上記で記述した自分ルールをもとに以下のようなテストメソッドを用意しました。
f:id:jarinosuke0808:20120219145851p:image

実装は以下になります。
また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 クラスの動作は保証された事になります。やりましたね!

テストを書いた。

テストを書くのは面倒ですが、決して無駄にはならない作業のはずです。
上記で扱ったプロジェクトは 僕の Github に上げてあります。
次回は GHUnit というサードパーティのテストライブラリと、
OCMock というこれまたサードパーティ製の Unit Testing の手助けとなるモックオブジェクトを使って一歩進んだ Unit Test をご紹介したいと思います。