4

主に依存性注入のために、Core Dataを使用してオブジェクトのグラフを管理しようとしています(NSManagedObjectsのサブセットは永続化する必要がありますが、それは私の質問の焦点では​​ありません)。単体テストを実行するときは、NSManagedObjectsの作成を引き継ぎ、モックに置き換えたいと思います。

今のところ、これを行うための候補となる手段があります。これは、ランタイムのmethod_exchangeImplementationsを使用し[NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]て、自分の実装と交換することです(つまり、モックを返す)。これは、私が行った小さなテストで機能します。

これに関して2つの質問があります。

  1. insertNewObjectForEntityForName:inManagedObjectContextをスウィズリングするよりも、Core Dataのオブジェクト作成を置き換えるためのより良い方法はありますか?私はランタイムやコアデータに深く踏み込んでおらず、明らかな何かが欠けている可能性があります。
  2. 私の置換オブジェクト作成メソッドの概念は、モックされたNSManagedObjectsを返すことです。私はOCMockを使用していますが、動的な@propertysのため、NSManagedObjectサブクラスを直接モックすることはありません。今のところ、NSManagedObjectのクライアントは具象オブジェクトではなくプロトコルと通信しているため、具象オブジェクトではなくモックされたプロトコルを返します。もっと良い方法はありますか?

これが私が得ているものを説明するためのいくつかの疑似的なコードです。これが私がテストしているかもしれないクラスです:

@interface ClassUnderTest : NSObject 
- (id) initWithAnObject:(Thingy *)anObject anotherObject:(Thingo *)anotherObject;
@end


@interface ClassUnderTest()
@property (strong, nonatomic, readonly) Thingy *myThingy;
@property (strong, nonatomic, readonly) Thingo *myThingo;
@end

@implementation ClassUnderTest
@synthesize myThingy = _myThingy, myThingo = _myThingo;
- (id) initWithAnObject:(Thingy *)anObject anotherObject:(Thingo *)anotherObject {

    if((self = [super init])) {
        _myThingy = anObject;
        _myThingo = anotherObject;
    }

    return self;
}
@end

おそらく永続性などのために、ThingyおよびThingo NSManagedObjectサブクラスを作成することにしましたが、initを次のようなものに置き換えることもできます。

@interface ClassUnderTest : NSObject 
- (id) initWithManageObjectContext:(NSManagedObjectContext *)context;
@end

@implementation ClassUnderTest
@synthesize myThingy = managedObjectContext= _managedObjectContext, _myThingy, myThingo = _myThingo;
- (id) initWithManageObjectContext:(NSManagedObjectContext *)context {

    if((self = [super init])) {
        _managedObjectContext = context;
        _myThingy = [NSEntityDescription insertNewObjectForEntityForName:@"Thingy" inManagedObjectContext:context];
        _myThingo = [NSEntityDescription insertNewObjectForEntityForName:@"Thingo" inManagedObjectContext:context];
    }

    return self;
}
@end

次に、ユニットテストで次のようなことができます。

- (void)setUp {
    Class entityDescrClass = [NSEntityDescription class];
    Method originalMethod = class_getClassMethod(entityDescrClass,  @selector(insertNewObjectForEntityForName:inManagedObjectContext:));
    Method newMethod = class_getClassMethod([FakeEntityDescription class],  @selector(insertNewObjectForEntityForName:inManagedObjectContext:));
    method_exchangeImplementations(originalMethod, newMethod);

}

...[]FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext]実際のNSManagedObjects(またはそれらが実装するプロトコル)の代わりにモックを返します。これらのモックの唯一の目的は、ClassUnderTestの単体テスト中に行われた呼び出しを検証することです。すべての戻り値はスタブされます(他のNSManagedObjectsを参照するゲッターを含む)。

したがって、私のテストClassUnderTestインスタンスは単体テスト内に作成されます。

ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];

(私のスウィズルのため、コンテキストは実際にはテストで使用されませんinsertNewObjectForEntityForName:inManagedObjectContext

このすべてのポイント?とにかく多くのクラスでコアデータを使用するので、コンストラクターの変更を管理する負担を軽減するために使用することもできます(コンストラクターを変更するたびに、単体テストの束を含むすべてのクライアントを編集する必要があります)。Core Dataを使用していなかった場合は、異議申し立てのようなものを検討するかもしれません。

4

4 に答える 4

3

コアデータエンティティを含むテストには、一般的に2つのタイプがあります。1)エンティティを引数として取るテストメソッドと、2)コアデータエンティティのCRUD操作を実際に管理するテストメソッドです。

#1については、@ graham-leeが推奨するように、私はあなたがしているように聞こえることを行います。エンティティのプロトコルを作成し、そのプロトコルをテストでモックします。コードが追加される方法がわかりません。プロトコルでプロパティを定義し、エンティティクラスをプロトコルに準拠させることができます。

@protocol CategoryInterface <NSObject>

@property(nonatomic,retain) NSString *label;
@property(nonatomic,retain) NSSet *items;
@property(nonatomic,retain) NSNumber *position;

@end

@interface Category : NSManagedObject<CategoryInterface> {}

@end

#2に関しては、私は通常、単体テストでメモリ内ストアを設定し、メモリ内ストアを使用して機能テストをテストします。

static NSManagedObjectModel *model;
static NSPersistentStoreCoordinator *coordinator;
static NSManagedObjectContext *context;
static NSPersistentStore *store;
CategoryManager *categoryManager;

-(void)setUp {
    [super setUp];
    // set up the store
    NSString *userPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"category" ofType:@"momd"];
    NSURL *userMomdURL = [NSURL fileURLWithPath:userPath];
    model = [[NSManagedObjectModel alloc] initWithContentsOfURL:userMomdURL];
    coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    store = [coordinator addPersistentStoreWithType: NSInMemoryStoreType
                                      configuration: nil
                                                URL: nil
                                            options: nil 
                                              error: NULL];
    context = [[NSManagedObjectContext alloc] init];

    // set the context on the manager
    [context setPersistentStoreCoordinator:coordinator];
    [categoryManager setContext:context];
}

-(void)tearDown {    
    assertThat(coordinator, isNot(nil));
    assertThat(model, isNot(nil));
    NSError *error;
    STAssertTrue([coordinator removePersistentStore:store error:&error], 
                 @"couldn't remove persistent store: %@", [error userInfo]);
    [super tearDown];
}

コーディネーターとモデルが正常に作成されたことを確認しtearDownます。作成によって例外がスローされたことがありsetUp、テストが実際に実行されていなかったためです。これはその種の問題を捕らえるでしょう。

于 2011-07-31T05:35:50.043 に答える
2

これに関するブログ投稿は次のとおりです:http: //iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

ideveloper.tvサイトには、coredataを含む多くのcocoaフレームワークで単体テストを行う方法について説明したトレーニングビデオがあります:http://ideveloper.tv/store/details?product_code = 10007

于 2011-07-30T12:49:22.703 に答える
1

オブジェクトグラフと管理対象オブジェクト自体が複雑になって正確にモックできるため、CoreDataのモックは嫌いです。代わりに、本格的なリファレンスストアファイルを生成して、それに対してテストすることを好みます。それはより多くの作業ですが、結果はより良いです。

アップデート:

insertNewObjectForEntityForName:inManagedObjectContextをスウィズリングするよりも、Core Dataのオブジェクト作成を置き換えるためのより良い方法はありますか?

クラス、つまり単一のインスタンスを分離してテストしたいだけの場合は、オブジェクトをコンテキストに挿入する必要はまったくありません。代わりに、他のオブジェクトと同じように初期化できます。アクセサーおよびその他のメソッドは通常どおり機能しますが、変更を監視し、オブジェクトと他の「管理対象」オブジェクトとの関係を「管理」するコンテキストはありません。

私の置換オブジェクト作成メソッドの概念は、モックされたNSManagedObjectsを返すことです。私はOCMockを使用していますが、動的な@propertyがあるため、NSManagedObjectサブクラスを直接モックすることはありません。今のところ、NSManagedObjectのクライアントは具象オブジェクトではなくプロトコルと通信しているので、具象オブジェクトではなくモックされたプロトコルを返します。もっと良い方法はありますか?

それはあなたが実際に何をテストしているかによります。NSManagedObjectサブクラス自体をテストしている場合、モックプロトコルは役に立ちません。管理対象オブジェクトと通信したり操作したりする他のクラスをテストしている場合は、モックプロトコルが正常に機能します。

Core Dataをテストするときに把握しておくべき重要なことは、Core Dataの複雑さは、実行時のオブジェクトグラフの作成にあるということです。属性の取得と設定は簡単です。複雑になるのは、関係とKey-Valueの監視です。後者を正確にモックすることはできません。そのため、テストする参照オブジェクトグラフを作成することをお勧めします。

于 2011-07-30T14:16:46.800 に答える
1

サンプルコードを見ると、Core Data APIの詳細でテストが行​​き詰まっており、その結果、テストを解読するのは簡単ではないようです。気になるのは、CDオブジェクトが作成されたことだけです。私がお勧めするのは、CDの詳細を抽象化することです。いくつかのアイデア:

1)CDオブジェクトの作成をラップするインスタンスメソッドをClassUnderTestで作成し、それらをモックします。

ClassUnderTest *thingyMaker = [ClassUnderTest alloc];
id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker];
[[[mockThingyMaker expect] andReturn:mockThingy] createThingy];

thingyMaker = [thingyMaker initWithContext:nil];

assertThat([thingyMaker thingy], sameInstance(mockThingy));

2)ClassUnderTestのスーパークラスにのような便利なメソッドを作成します-(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;。次に、部分的なモックを使用して、そのメソッドの呼び出しをモックできます。

ClassUnderTest *thingyMaker = [ClassUnderTest alloc];
id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker];
[[[mockThingyMaker expect] andReturn:mockThingy] createManagedObjectOfType:@"Thingy" inContext:[OCMArg any]];

thingyMaker = [thingyMaker initWithContext:nil];

assertThat([thingyMaker thingy], sameInstance(mockThingy));

3)一般的なCDタスクを処理するヘルパークラスを作成し、そのクラスへの呼び出しをモックします。私はいくつかのプロジェクトでこのようなクラスを使用しています:

@interface CoreDataHelper : NSObject {}

+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;
+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate;
+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate sortedBy:(NSArray *)sortDescriptors;
+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate sortedBy:(NSArray *)sortDescriptors limit:(int)limit;
+(NSManagedObject *)findManagedObjectByID:(NSString *)objectID inContext:(NSManagedObjectContext *)context;
+(NSString *)coreDataIDForManagedObject:(NSManagedObject *)object;
+(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;    

@end

これらはモックするのが難しいですが、比較的簡単なアプローチについては、クラスメソッドのモックに関する私のブログ投稿をチェックしてください。

于 2011-08-04T05:25:17.560 に答える