Как использовать базовые данные для внедрения зависимостей - PullRequest
4 голосов
/ 30 июля 2011

Я играю с использованием Core Data для управления графом объектов, главным образом для внедрения зависимостей (подмножество NSManagedObjects должно сохраняться, но это не главное в моем вопросе).Когда я запускаю модульные тесты, я хочу взять на себя создание NSManagedObjects, заменив их на mocks.

У меня сейчас есть подходящее средство для этого, которое заключается в использовании метода method_exchangeImplementations во время выполнения для обмена [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]с моей собственной реализацией (т. е. возвращая издевается).Это работает для небольшого теста, который я сделал.

У меня есть два вопроса по этому поводу:

  1. Есть ли лучший способ заменить создание объекта Core Data, чем swizzling insertNewObjectForEntityForName: inManagedObjectContext?Я не углубился в среду выполнения или Core Data и, возможно, упускаю что-то очевидное.
  2. Моя концепция метода создания замещающего объекта состоит в том, чтобы возвращать ложные NSManagedObjects.Я использую OCMock, который не будет напрямую насмехаться над подклассами NSManagedObject из-за их динамических @property s.Пока клиенты моего 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 (или протоколов, которые они реализуют).Цель only этих проверок состоит в том, чтобы проверять вызовы, сделанные им во время модульного тестирования ClassUnderTest.Все возвращаемые значения будут заглушены (включая любые методы получения, ссылающиеся на другие NSManagedObjects).

Мои тестовые ClassUnderTest экземпляры будут созданы в модульных тестах, таким образом:

ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];

(контекст на самом деле не будет использоваться в тесте, из-за моего крутого insertNewObjectForEntityForName:inManagedObjectContext)

Смысл всего этого?В любом случае, я собираюсь использовать Core Data для многих классов, поэтому я мог бы также использовать его, чтобы уменьшить нагрузку, связанную с управлением изменениями в конструкторах (каждое изменение в конструкторе включает редактирование всех клиентов, включая несколько модульных тестов).Если бы я не использовал Базовые данные, я мог бы рассмотреть что-то вроде Возражение .

Ответы [ 4 ]

3 голосов
/ 31 июля 2011

Я считаю, что обычно есть два типа тестов, которые включают сущности Core Data: 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, поэтому тесты фактически не выполнялись. Это поймает такую ​​проблему.

2 голосов
/ 30 июля 2011

Вот запись в блоге об этом: http://iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

На сайте ideveloper.tv есть обучающее видео, в котором рассказывается, как выполнять модульное тестирование во многих инфраструктурах какао, включая coredata: http://ideveloper.tv/store/details?product_code=10007

1 голос
/ 04 августа 2011

Глядя на ваш пример кода, мне кажется, что ваш тест увяз в деталях API Core Data, и в результате тест не так просто расшифровать.Все, что вас волнует, это то, что объект CD был создан.Что я бы порекомендовал, так это абстрагировать детали CD.Несколько идей:

1) Создайте методы экземпляра в ClassUnderTest, которые обертывают создание ваших объектов CD, и смоделируйте их:

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

Они более хитры, но вы можете проверить мой пост в блоге о методах насмешливых классов для относительно простого подхода.

1 голос
/ 30 июля 2011

Мне не нравятся макеты для Базовых Данных, потому что граф объектов и сами управляемые объекты могут быть сложными, чтобы точно имитировать. Вместо этого я предпочитаю создать файл полного справочного хранилища и проверить его. Это больше работы, но результаты лучше.

Обновление:

есть ли лучший способ заменить создание объекта Core Data, чем swizzling insertNewObjectForEntityForName: inManagedObjectContext?

Если вы просто хотите протестировать класс, т. Е. Отдельный экземпляр, тогда вам вообще не нужно вставлять объект в контекст. Вместо этого вы можете просто инициализировать его как любой другой объект. Средства доступа и другие методы будут работать в обычном режиме, но просто нет контекста, наблюдающего за изменениями и «управляющего» отношениями объекта с другими «управляемыми» объектами.

моя концепция метода создания замещающего объекта - возвращать макет NSManagedObjects. Я использую OCMock, который не будет напрямую издеваться NSManagedObject подклассы из-за их динамического @propertys. За теперь клиенты моего NSManagedObject общаются с протоколами, а не конкретные объекты, поэтому я возвращаю проверенные протоколы, а не конкретные объекты. Есть ли лучший способ?

Это зависит от того, что вы на самом деле тестируете. Если вы тестируете сам подкласс NSManagedObject, то фиктивный протокол бесполезен. Если вы тестируете другие классы, которые взаимодействуют с управляемым объектом или манипулируют им, то фиктивный протокол будет работать нормально.

При тестировании Core Data важно понимать, что сложность Core Data заключается в построении графа объектов во время выполнения. Получение и установка атрибутов тривиальны, усложняются отношения и наблюдение значения ключа. Вы действительно не можете издеваться над последним с какой-либо точностью, поэтому я рекомендую создать граф контрольных объектов для проверки.

...