テストにはテスト内容に応じていろいろな種類や方法がありますが、オブジェクト単位のシンプルな機能テストと言えばxUnitなどを使うユニットテストがあります。オブジェクトに何かを入力したときの出力を検査することで、オブジェクトの機能をテストするというものです。
MVCパターンに基づいてコードを書いている場合、Modelが最もテストし易いコンポーネントになります。Modelは他のコンポーネントに依存しない形になっているので、テストプログラムに組み込むことも簡単なのです。
逆に、ViewやControllerはテストしにくいコンポーネントです。まあ、ViewやControllerに限らず、I/O処理を含むコードのテストはModelであっても困難です。ただModelであればまだ手段がありますが、特にViewに関してはグラフィックの描画という部分がOSに結び付いているため、この部分のテストを行うのは非常に困難です。
でも、だからといってViewをテストしないわけにも行きません。
まず最初に考えたのは、グラフィックの描画そのものをテストすることはできないが、描画に使用するパラメータならテストできるのではないか、という事です。例えば、NSRectFill()で塗りつぶす時に矩形の座標情報がパラメータとして必要になります。塗りつぶした結果はテストできないかもしれませんが、矩形の座標情報ならテスト可能ですし、それをテストすれば塗りつぶした結果を間接的にテストしたことになるのでしょう。
しかしこの方法はうまく行きませんでした。なぜなら、Modelの場合は「テストに合格するコードが良いコード」だったのに対し、Viewの場合は「見栄えの良いグラフィックを描画するコードが良いコード」だからです。グラフィックは実際に画面に表示させて見ないと評価できません。座標情報を前もって決めておくことは困難なのです。
このように、グラフィックの描画がViewの主要機能であるにもかかわらず、それをテストすることが現実的ではないとすると、そもそもViewの何をテストすればいいのでしょうか。いろいろ考えた結果、おそらく以下のようなものがテストの対象になるのでしょう。
・ViewがModelオブジェクトにアクセスできるか?
・ViewがModelオブジェクトからの通知を受け取れるか?
・ViewがNSEventを受け取りTarget/Actionを発行できるか?
しかし、これはこれで難しいデストです。通常ユニットデストでは、あるものを入力したときの出力をテストしますが、上記のような場合は出力されるものがありません。そこで、これをテストするために、以下のような方法を考えました。
@interface TestView : MyView {
NSMutableArray* log;
}
@property(retain) NSMutableArray* log;
@end
@implementation TestView
@synthesize log;
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
[log addObject:@"observeValueForKeyPath"];
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
- (void)setNeedsDisplay:(BOOL)flag {
[log addObject:@"setNeedsDisplay"];
}
@end
@implementation MyTestCase
- (void)testKeyValueObserv {
MyModel* model = [[MyModel alloc] init];
TestView* view = [[TestView alloc] initWithFrame:NSZeroRect];
[view bind:@"content" toObject:model withKeyPath:@"self" options:nil];
view.log = [[NSMutableArray alloc] init];
[model setValue:@"hoge" forKey:@"boke"];
STAssertEqualObjects(
[view.log objectAtIndex:0], @"observeValueForKeyPath", nil);
STAssertEqualObjects(
[view.log objectAtIndex:1], @"setNeedsDisplay", nil);
}
@endこれは、テスト対象のViewオブジェクトに、メソッド呼び出しのログを取る機能を追加して、ユニットテストを行うという方法です。このテストにより、Modelオブジェクトのプロパティが変化したときに、Viewがその通知を受け取って再描画しようとしている動きをテストできます。
メソッドの呼び出しがログという形で出力されるので、通常のユニットテストでもテスト可能になります。また、上記の例ではsetNeedsDisplay:の呼び出しでsuperを呼んでいないので、実際にグラフィックの描画というI/O処理が発生しません。
これにより、Viewのテストができる見通しがたったものの、メソッドのログを取るためにいちいちサブクラスを書くのはちょっと面倒。でも、このようなことは、アスペクト指向を使えば簡単なのですが、残念ならがObjective-Cにはアスペクト指向の実装がありません。昔は、あったらしいですけどね、なんとなく復活してくれるとうれしいです。