読者です 読者をやめる 読者になる 読者になる

yashigani?.days

週刊少年ジャンプについてだらだら書きます

OCMockでNSManagedObjectをstubする

NSManagedObjectのプロパティをstubしようと思ってめっちゃはまった. こんな風に普通にstubしようとしてもstubできない.

// こんなNSManagedObjectのサブクラスがあるとする
// @interface Event : NSManagedObject
// @property (nonatomic, retain) NSDate * timeStamp;
// @property (nonatomic, retain) NSString * title;
// @end

id mock = [OCMockObject mockForClass:Event.class];
[[[mock stub] andReturn:@"test"] title];
XCTAssertTrue([[mock title] isEqualToString:@"test"], @":(");

これだとunrecognized selectorが発生して異常終了してしまう. stubしたはずなのに,なぜunrecognized selectorが発生するかというと,NSManagedObjectのサブクラスで自動生成されるプロパティは@dynamicだから. つまり,そもそもが実行時にいい感じに解決される仕組みなのでOCMockではstubできないらしい.

解決策

1. valueForKey:を使う

valueForKey:はdynamicに解決されるメソッドでないのでstubできる.

[[[mock stub] andReturn:@"test"] valueForKey:@"title"];
XCTAssertTrue([[mock valueForKey:@"title"] isEqualToString:@"test"], @":(");

これはかなりスマートな解決策で超Coolなんだけど,テスト対象の実装のほうでもvalueForKey:を使って実装する必要があるのがいけてない. NSStringFromSelectorとかを使えばある程度回避できるけど,valueForKey:だとどうしてもData Modelの構成を変更するときに修正漏れがありそうなので,できるだけ避けたい.

2. Protocolを使う

NSManagedObjectと同じプロパティを持つProtocolを宣言し,それを代わりに使うというアプローチ. Protocolは普通にstubできるのでunrecognized selectorは回避できる.

// このようなプロトコルを用意する
// @protocol Event <NSObject>
// @property (strong, nonatomic) NSDate *timeStamp;
// @property (strong, nonatomic) NSString *title;
// @end

id mock = [OCMockObject mockForProtocol:@protocol(Event)];
[[[mock stub] andReturn:@"test"] title];
XCTAssertTrue([[mock title] isEqualToString:@"test"], @":(");

こちらもアプローチとしてかなり良い. ただ,NSManagedObjectに変更をした場合に忘れずstub用のProtocolも直す必要がある.

関連

雑談コーナー

実装方法を束縛しないという点で2のアプローチが良い. 一歩踏み込んで,Protocolは実行時に動的に作ることができるから,stubしたいNSManagedObjectからstub用のProtocolを作って使うことができる. すると,変更に伴って自動でstub用のProtocolも変更されるのでめっちゃいい気がした.

Protocolを動的に作ることに関しては,以下が参考になる.

しかし,よくよく考えると,そもそも実装のほうを変更したらテストは落ちてしかるべきではないのだろうか. Protocolを動的に生成するの挫折した難しいし,テストあるべき論みたいなところで止まってしまっている.