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を動的に作ることに関しては,以下が参考になる.
- Conference with DevelopersでJavaScriptCore.frameworkとObjective-C Runtime APIについて話しました - 24/7 twenty-four seven
- addProtocol.m
しかし,よくよく考えると,そもそも実装のほうを変更したらテストは落ちてしかるべきではないのだろうか.
Protocolを動的に生成するの挫折した難しいし,テストあるべき論みたいなところで止まってしまっている.