Cómo usar los datos básicos para la dependency injection

Estoy jugando con el uso de Core Data para administrar un gráfico de objects, principalmente para la dependency injection (un subset de los objects NSManagedObjects debe persistir, pero ese no es el foco de mi pregunta). Al ejecutar las testings unitarias, quiero tomar el control de la creación de NSManagedObjects, reemplazándolas por simulaciones.

Tengo un medio candidato para hacer esto por ahora, que es usar el method_exchangeImplementations del time de ejecución para intercambiar [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:] con mi propia implementación (es decir, devolver imitaciones). Esto funciona para una pequeña testing que he hecho.

Tengo dos preguntas con respecto a esto:

  1. ¿Hay una mejor manera de replace la creación de objects de Core Data que swizzling insertNewObjectForEntityForName: inManagedObjectContext? No he entrado mucho en el time de ejecución o Core Data, y puede que falte algo obvio.
  2. Mi concepto de método de creación de objects de reemploop es devolver simulados NSManagedObjects. Estoy usando OCMock, que no se burlará directamente de las subclasss NSManagedObject debido a su dinámica @property s. Por ahora, los clientes de mi NSManagedObject están hablando con protocolos en lugar de con objects concretos, por lo que devuelvo protocolos simulados en lugar de objects concretos. ¿Hay una mejor manera?

Aquí hay un código pseudo para ilustrar lo que estoy obteniendo. Aquí hay una class que podría estar probando:

 @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 

Decido crear subclasss Thingy y Thingo NSManagedObject, tal vez para la persistencia, etc., pero también para replace el init con algo como:

 @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 

Luego, en las testings de mi unidad, puedo hacer algo como:

 - (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); } 

… donde mi []FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext] devuelve []FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext] en lugar de NSManagedObjects reales (o protocolos que implementan). El único propósito de estos simulacros es verificar las llamadas realizadas a ellos mientras se testing en la unidad ClassUnderTest. Todos los valores de retorno serán aplastados (incluidos los captadores que se refieren a otros NSManagedObjects).

Mis testings ClassUnderTest instancias de ClassUnderTest se crearán dentro de las testings de unidades así:

ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];

(el context no se utilizará en la testing, debido a mi swizzled insertNewObjectForEntityForName:inManagedObjectContext )

¿El punto de todo esto? De todos modos, voy a utilizar Core Data para muchas de las classs, así que también podría usarlo para ayudar a networkingucir la carga de la gestión de los cambios en los constructores (cada cambio de constructor implica editar todos los clientes, incluido un montón de testings de unidades). Si no estuviera usando Core Data, podría considerar algo como Objeción .

Mirando su código de muestra, me parece que su testing se está atascando en los detalles de la API de Core Data, y como resultado, la testing no es fácil de descifrar. Todo lo que te importa es que se haya creado un object de CD. Lo que recomiendo es resumir los detalles del CD. Algunas ideas:

1) Cree methods de instancia en ClassUnderTest que envuelven la creación de sus objects de CD y se burlan de ellos:

 ClassUnderTest *thingyMaker = [ClassUnderTest alloc]; id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker]; [[[mockThingyMaker expect] andReturn:mockThingy] createThingy]; thingyMaker = [thingyMaker initWithContext:nil]; assertThat([thingyMaker thingy], sameInstance(mockThingy)); 

2) Cree un método de conveniencia en la superclass ClassUnderTest, como -(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; . Entonces puedes simular llamadas a ese método usando una simulación parcial:

 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) Cree una class auxiliar que maneje tareas de CD comunes y simule las llamadas a esa class. Utilizo una class como esta en algunos de mis proyectos:

 @interface CoreDataHelper : NSObject {} +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPnetworkingicate:(NSPnetworkingicate *)pnetworkingicate; +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPnetworkingicate:(NSPnetworkingicate *)pnetworkingicate sortedBy:(NSArray *)sortDescriptors; +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPnetworkingicate:(NSPnetworkingicate *)pnetworkingicate 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 

Estos son más complicados de burlarse, pero puedes revisar mi publicación de blog sobre methods burocráticos de class para un enfoque relativamente sencillo.

Me parece que en general hay 2 types de testings que involucran a las entidades de Core Data: 1) methods de testing que toman una entidad como argumento, y 2) methods de testing que realmente administran las operaciones CRUD en las entidades de datos centrales.

Para el # 1, hago lo que parece que estás haciendo, como @ graham-lee recomienda : crea un protocolo para tus entidades y simula ese protocolo en tus testings. No veo cómo agrega ningún código adicional: puede definir las properties en el protocolo y hacer que la class de entidad se ajuste al protocolo:

 @protocol CategoryInterface <NSObject> @property(nonatomic,retain) NSString *label; @property(nonatomic,retain) NSSet *items; @property(nonatomic,retain) NSNumber *position; @end @interface Category : NSManagedObject<CategoryInterface> {} @end 

En cuanto al número 2, generalmente configuro un almacén en memory en las testings de mi unidad y simplemente hago testings funcionales usando una tienda en memory.

 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]; } 

Verifico en tearDown que el coordinador y el model fueron creados con éxito, porque encontré que hubo ocasiones en que la creación lanzó una exception en setUp , por lo que las testings no se estaban ejecutando realmente. Esto capturará ese tipo de problema.

Aquí hay una publicación en este blog: http://iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

Hay un video de entrenamiento en el sitio ideveloper.tv que menciona cómo hacer testings unitarias en muchos de los frameworks de cocoa, incluyendo conetworkingata: http://ideveloper.tv/store/details?product_code=10007

No me gustan las simulaciones de Core Data porque el gráfico de objects y los objects gestionados pueden ser complejas para simular con precisión. En cambio, prefiero generar un file de reference de tienda de reference completa y probar contra eso. Es más trabajo, pero los resultados son mejores.

Actualizar:

¿Hay una mejor manera de replace la creación de objects de Core Data que swizzling insertNewObjectForEntityForName: inManagedObjectContext?

Si solo desea probar la class, es decir, una única instancia aislada, no tiene que insert el object en ningún context. En cambio, puede inicializarlo como cualquier otro object. Los accesores y otros methods funcionarán de manera normal, pero simplemente no hay un context que observe los cambios y "administre" la relación del object con otros objects "gestionados".

Mi concepto de método de creación de objects de reemploop es devolver simulados NSManagedObjects. Estoy usando OCMock, que no se burlará directamente de las subclasss NSManagedObject debido a su dinámica @propertys. Por ahora, los clientes de mi NSManagedObject están hablando con protocolos en lugar de con objects concretos, por lo que devuelvo protocolos simulados en lugar de objects concretos. ¿Hay una mejor manera?

Depende de lo que estés probando. Si está probando la subclass NSManagedObject, entonces el protocolo falso es inútil. Si está probando otras classs que se comunican o manipulan el object gestionado, el protocolo falso funcionará bien.

La cuestión importante a tener en count al probar Core Data es que la complicada complejidad en Core Data se produce en la construcción del gráfico de object en time de ejecución. La obtención y configuration de los attributes es trivial, son las relaciones y la observación del valor key lo que se complica. Realmente no puedes burlarte de este último con ninguna precisión, por eso recomiendo crear un gráfico de object de reference para probar.