UsingOCUnit

TopPage

OCUnit

HeaderDoc

GNUstep

Xcode

EmacsWiki


PageList

ChangeLog

Objective-C,ProjectBuilder,OCUnitを使った Unit Test

By: Marco Scheurer Sen:te, Lausanne, Switzerland June 17, 2002 - 04:10 AM

OCUnitのインストール
最初の例題
次の例題:乱数ジェネレーターのテスト
3番めの例題:UIのテスト
テストのデバッグ
要約
Acknowledgements

1994年に書かれたKent BeckのSmalltalkユニット・テスト・フレームワーク以来、あるいは最近のXPの人気のために、ユニット・テストはコードの品質を改善するための有名な技術になりました。

この論文は実践的なイントロダクションです。どうやってProjectBuilderとOCUnitを使ってユニット・テストを行うか、どうやって迅速なコーディング-コンパイル-テストのターン・アラウンドを達成するかを教えます。

この論文の例題を実行するためにはシステムにOCUnitをインストールする必要があります。 OCUnitはアップルの開発ツールにObjective-C用ユニット・テストを実装するフレームワーク、ツールおよびリソース・コードのコレクションです。 OCUnitのディストリビューションには他の例題と一緒に、この論文の中で使用されるプロジェクトを含んでいます。

この論文は、WWDC 2002the Stepwise Birds of Feather meetingのプレゼンテーションに基づきます。

OCUnitのインストール

OCUnitは http://www.sente.ch/software/ocunit/ から利用可能です。 全てのソース・コードは利用可能です。しかし、OCUnitはプロジェクトとクラスのテンプレートを正しい場所に置くインストーラー・パッケージが付属します。

プロジェクト・テンプレートはそれらの標準のフレームワークやCocoaに似ているが、プロジェクトにユニット・テストを含むのに必要なメカニズムを含んでいます。

  • otest toolを起動するシェル・スクリプトはメイン・ターゲットのビルド・パースに追加されます
  • テスト・ビルド・スタイルをセットして構築すると、ビルドした物はotestの起動を有効にします。
  • SenFoundationとSenTestingKitフレームワーク

最初の例題

この例はPersonプロジェクト(OCUnitのディストリビューションの例題として入っています)を使用します。 このフレームワーク・プロジェクトは、2つの属性、firstNameおよびlastName用のaccessor方法と共に、基本的なPersonクラスを含んでいます。 私たちはこのクラスに単純な新しい人の姓名を返す能力を加えます。 「テスト・ファースト」哲学に従って、私たちが期待する結果について記述するテストを加えることから始めます。

最初のテストは、期待する結果について記述するでしょう:Personに、firstName「ピカソ」でlastName「パブロ」であれば、彼のFullNameは「パブロ・ピカソ」であるべきです。

OCUnitではテスト・ケースはSenTestCaseのサブクラスとして実装されています。 「Test」で始まる名前で、引数なし、返り値なしのメソッドです。

 #import "TestPerson.h"
 #import "Person.h"
 
 @implementation TestPerson
 - (void) testFullName
 {
     Person *person = [[Person alloc] init];    
     [person setFirstName:@"Pablo"];
     [person setLastName:@"Picasso"];
     should ([[person fullName] isEqual:@"Pablo Picasso"]);
     [person release];
 }
 @end

"should"ステートメントは我々が望む結果を表現する宣言方法です。JavaのJUnitではこれに"assert"を使ってます。しかし、私たちは、コードの中で"assert"を区別することを好みます。通過するべきテストに失敗すれば、コードは例外処理を出します。 OCUnitにはshouldBeEqual()やshouldRaiseException()等の特殊化された"should"ステートメントがります.

結果について記述するために今いくつかのテストを加えましょう。私たちはfirstNameがnilかEmptyである事を望んでます(さらにlastNameの実装でも同じです)。

 #import "TestPerson.h"
 #import "Person.h"
 
 @implementation TestPerson
 - (void) testFullName
 {
     Person *person = [[Person alloc] init];	
     [person setFirstName:@"Pablo"];
     [person setLastName:@"Picasso"];
     should ([[person fullName] isEqual:@"Pablo Picasso"]);
     [person release];
 }
 
 - (void) testEmptyFirstName
 {
     Person *person = [[Person alloc] init];    
     [person setFirstName:@""];
     [person setLastName:@"Picasso"];
     shouldBeEqual ([person fullName], [person lastName]);
     [person release];
 }
 
 - (void) testNilFirstName
 {
     Person *person = [[Person alloc] init];    
     [person setFirstName:nil];
     [person setLastName:@"Picasso"];
     shouldBeEqual ([person fullName], [person lastName]);
     [person release];
 }
 @end

「テスト」ビルド・スタイルを選択し、プロジェクトを構築して、何が起こるか確かめてください。

http://graphics.stepwise.com/Articles/Technical/OCUnit-001.gif

驚くべき事ではないが、コンパイラーはfullNameメソッドが未知であるとの警告をだし、テスト・キットはfullNameセレクターが認識されないとのランタイム・エラーを出します。

>注:上記のProjectBuilderのスクリーンキャプチャーではTestコントロールがToolBarに見えています。これをするためには、ProjectBuilderのプライベート・フレームワークファイルを修正しなければなりませんでした。OCUnitのデストリュビューションには、この改造方法についての指示を含んでいますが、これは各自のリスクで行うべき事です。これはOCUnitのインストーラー・パッケージによって行われません。(ProjectBuilderIntegration/PBX/READMEを参照してください)

これらの警告およびエラーを取り除くためにPersonクラスにfullNameメソッドを追加します。Person.hに以下の内容を追加してください。

 - (NSString *) fullName;
そしてPerson.mにも追加する
 - (NSString *) fullName
 {
     return @"";
 }

再びビルドするとプロジェクトは以下のような結果を帰す:

 Test.m:12: -[Test testFullName] :\
               [[person fullName] isEqual:Pablo Picasso ] should be true
 Test.m:22: -[Test testNilFirstName] : '' should be equal to 'Picasso' 
 Test.m:32: -[Test testNilFirstName] : '' should be equal to 'Picasso'

結果は異なります:テストの実行は、fullNameメソッドが、私たちが指定したことを行っていないことを示しています(実装してないのだから、驚くべき事ではない)。 しかし、少なくとも私たちは「セレクターを認識しません」と言うようなランタイム例外をだしてはいません。

fullNameの正しい実装ははNSStringのstringWithFormat:を使用して以下のようになります。

 - (NSString *) fullName
 {
     return [NSString stringWithFormat:@"%@ %@", 
                                      [self firstName], 
                                      [self lastName]];
 }
再びビルドすると、今度の状況では2つしかテストが失敗しない。
 Test.m:22: -[Test testEmptyFirstName] : \
                  ' Picasso' should be equal to 'Picasso' 
 Test.m:32: -[Test testNilFirstName] : \
                  '(null) Picasso' should be equal to 'Picasso'

第1のエラーはfullNameの中の主要なスペースが原因です。 第2のエラーはstringWithFormatへのnil引数が原因です。このようなfirstNameとfullNameの方法の変更によりこれらの2つの問題を解決しましょう。

 - (NSString *) firstName
 {
     return firstName != nil ? firstName : @"";
 }
 
 - (NSString *) fullName
 {
     return [[NSString stringWithFormat:@"%@ %@",
                                        [self firstName],
                                        [self lastName]]
              stringByTrimmingSpace];
 }

プロジェクトのビルドはもうエラーを出しません。 Personクラスの中で書かれたコードが、私たちが書いたテストで表現された必要条件を満たすと確信出来ます。 PersonCompleteプロジェクトが含んでいるOCUnitのExample Folderでは、追加のテストの為にこれまでのコードをタイプしました。 TestPersonクラスは、ユニット・テストに合格した物を"test fixture"にします。 上記の例では、Personオブジェクトを作成したりリリースするとテストが実行され異常に気付く事が出来ます。 この共通のコードは-setUpと-tearDownメソッドは各テストの最初と最後に自動的に実行されます。

ビルドの出力結果を見るとテスト・プロセスに関してより多くの情報が示されています(テスト・スイートおよびテストケースの開始や終了等)。 テスト・スイートはテストの集合でOCUnitによって自動的に作成されます。

テスト・ケースはTestCaseのサブクラスを作るためのテンプレートを使用して、容易に作成することができます。

このようにして、OCUnitはフレームワーク、ツール、バンドル、アプリケーションをテストするために使用することができます。

次の例題:乱数ジェネレーターのテスト

TestRandomプロジェクトは、Mersenne Twisterの疑似乱数ジェネレーターのObjective-Cクラスでのカプセル化がアルゴリズムを壊さなかったことを確認するテストを含んでいます。このクラスおよびオリジナルのインプリメンテーションを使用して得られた2つの100,000の数列が比較されます。

 - (void) testSenRandomUnsignedLong
 {
     unsigned long count = SEQUENCE_LENGTH;
     while (count--) {
         should ([generator nextUnsignedLong] == igenrand());
     }
 }

もちろん、2つの乱数ジェネレーターは、テストのsetUp中で同じ乱数シードに初期化されます。

このプロジェクトはさらにパフォーマンス・テストを含んでいます。 経過タイムだけを測定しているので、これは正確な性能測定ではありません。 しかしながら、それらは面白い結果をあらわます。最適化されたObjective-Cバージョンは1000万の数を生成する場合Cによる実装より速く見えます。

3番めの例題:UIのテスト

しばしばユーザ・インターフェースをテストすることができないので、ユニット・テストには限界があると言われています。多くのUIは自動的にテストすることが困難か不可能であることは本当てす。しかし今までどおりいくつかのものをテストすることができます。

MikeFerrisのTextExtrasプロジェクトでは少数のテストを加えます。オリジナルのディストリビューションを修正するのではなく、これらのテストのための新しいプロジェクトを作成しました。この例は、あなたがTextExtrasをインストールしたと仮定します。 (http://www.lorax.com/FreeStuff/FreeStuff.html で見つける事が出来ます)

テスト・ビルド・スタイルを使ってTestExtrasプロジェクトを構築し、アプリケーションを起動、テスト、終了しましょう。

以前と同じように結果はProjectBuilderの中で報告されます。これはアプリケーションのテキスト・ビュー上で実行された自動テストの例です:

 - (void) testForward
 {
     NSString *text = @"A Stepwise Apple";
     [self setText:text];
     [textView setSelectedRange:NSMakeRange (1, 0)];
     shouldBeEqual ([self nextCompletionFrom:text], @"pple"); 
 }
手動でテストを行うには次の手順で行う事になります。

  • アプリケーションの起動
  • テキスト・ビューの中で"A Stepwise Apple"とタイプする
  • 最初の"A"の後ろにカーソルを即時に移動させる
  • "esc"キーをおしてEscape completionを行う
  • テキストの結果が"Apple Stepwise Apple"であることを視覚的にチェックする

このテスト・ケースや手動による他のケースは明らかに長く苦痛であり間違いを誘発するプロセスです。各コード修正の後にテストを行うことは、プログラマにとって余りにも非道です。しかしながら、OCUnitを使用すると、これらのテストは小さなオーバーヘッドで各ビルドで行なうことができます。

これはどのようにすればいいでしょうか?上記のテスト・コードは、どのようにユーザ・インターフェースに接続されますか? ユーザ・インターフェース・テストについては、ユーザ・インターフェース自体に埋め込むよりよい場所はありません。 OCUnitは、InterfaceBuilderのnibファイルにテスト・ケースを含むことを可能にするパレットを提供します。TestExtra.nibを開いてください。そうすれば、テキスト・ビューへのアウトレット・コネクションと共にTestExtrasの実例を見るでしょう

テストのデバッグ

テストのうちの1つが失敗しているので(ProjectBuilderが行うようにTextExtrasは完成可能性の中で循環しません)、私たちにテストをデバッグするのに有用で、ProjectBuilderからのテストを実行する代替方法を見させコードをテストした。 TestExtrasターゲットの"Executables"タブを選択してください。"SenTest Self"項目を選択してください。 コードに戻り、失敗するテストでブレークポイントを置いて、デバッガの中でプログラムを始めてください。コンソールはテストをトレースします。また、実行はブレークポイントで止まるでしょう。

この時、テストはビルド・プロセスの間にではなく実行の中にテストされるか、あるいはプロセスをデバッグします。 そのアプリケーションは「テスト・モード」で走ります。また、テストが実行されます。これはフレームワークやバンドルでも使用できotestツールはテスト・ドライバーの役割を果たします。

要約

ユニット・テストはコード品質を増大する強力なツールとして賛美されました。この記事が示すように、OCUnitツールとフレームワークはProjectBuilderからObjective-Cのためにユニット・テストを使用することを非常に容易にします。 バンドルあるいはアプリケーションからのテスト・ケースを見つけてそれらを実行するメカニズムに興味を持っていた読者はソース・コードを見る事をお勧めします。ソースコードはFree BSDライセンスの下で配布されています。

Acknowledgements

このプレゼンテーションおよび記事を準備する間のそれらの支援をしてくれたビンセント・ケーラーおよびアラン・フィッシャーに、およびOCUnitへのすべての寄稿家に感謝。

Marco Scheureは1990年以来CocoaとCocoaの元になった開発環境でプログラムしています。1994年には、彼がSen:teを共同設立しました。Sen:teは迅速なソフトウェア開発でしられるスイスはローザンヌにある会社です。彼にはmarco@sente.chで連絡することができます。


Updated: 2004-01-02 MindTools