Bienvenido a la republica independiente de las pruebas unitarias con Core Data

Post on 13-Jan-2015

1.251 views 0 download

description

Presentación para el NSCoder Night Madrid del 6 de marzo de 2013 por Jorge Ortiz y yo mismo. En esta presentación hablado de cómo se pueden hacer pruebas unitarias a un modelo en Core Data y cómo conseguir desacoplar este modelo de la forma en que hemos elegido que persista.

Transcript of Bienvenido a la republica independiente de las pruebas unitarias con Core Data

Bienvenido a la república independiente de las pruebas

unitarias con Core DataJorge D. Ortiz Fuentes (@jdortiz)

Alfonso Alba Garcia (@aalbagarcia)

viernes, 8 de marzo de 13

Agenda★MVC

★Implementación en Core Data

★Pruebas Unitarias

★Desacoplamiento

★Conclusiones

2

viernes, 8 de marzo de 13

El modelo a seguirviernes, 8 de marzo de 13

MVC★ Las vistas las proporciona Apple (aunque

nosotros podemos crear lasque necesitemos).★ El modelo debe contener toda la lógica de

negocio.★ Atención: Modelo de datos vs modelo de

negocio.★ El controlador debería la conexión de las

vistas con el modelo de negocio.★ No es necesario que sea / NO debería ser un

singleton. Se pasa de un controlador a otro. (Core Data: MOC o UIManagedDocument)

4

viernes, 8 de marzo de 13

Modelo autocontenido★ La forma más sencilla de evitar

duplicación de código y conseguir un comportamiento consistente.

★ Core Data incluye restricciones. P. ej., atributo opcional o no o cardinalidad de una relación.

★ Pero para añadir otra funcionalidad hay que añadir métodos.

5

viernes, 8 de marzo de 13

Usa Core Data, Lukeviernes, 8 de marzo de 13

Implementación del modelo de negocio★ Modificar modelo de datos ⇒

regenerar clases. Xcode sobreescribe ⇒

★ Soluciones:๏ Aprovechar el control de versiones

๏ mogenerator (http://rentzsch.github.com/mogenerator/) de W. Rentzsch

๏ Categorías

7

métodos añadidos

viernes, 8 de marzo de 13

Implementación del modelo de negocio★ Modificar modelo de datos ⇒

regenerar clases. Xcode sobreescribe ⇒

★ Soluciones:๏ Aprovechar el control de versiones

๏ mogenerator (http://rentzsch.github.com/mogenerator/) de W. Rentzsch

๏ Categorías

7

métodos añadidos

viernes, 8 de marzo de 13

Modelo NSCoderApp

viernes, 8 de marzo de 13

Group (Generado)★ #import <Foundation/Foundation.h>

#import <CoreData/CoreData.h>

@class Person;

@interface Group : NSManagedObject

@property (nonatomic, retain) NSString * name;@property (nonatomic, retain) NSString * notes;@property (nonatomic, retain) NSSet *members;@property (nonatomic, retain) NSManagedObject *meetings;@end

@interface Group (CoreDataGeneratedAccessors)

- (void)addMembersObject:(Person *)value;- (void)removeMembersObject:(Person *)value;- (void)addMembers:(NSSet *)values;- (void)removeMembers:(NSSet *)values;

@end

#import "Group.h"#import "Person.h"

@implementation Group

@dynamic name;@dynamic notes;@dynamic members;@dynamic meetings;

@end

viernes, 8 de marzo de 13

Categoría Group+Model★ #import "Group.h"

@interface Group (Model)

- (BOOL) isDuplicated;

@end

#import "Group+Model.h"

@implementation Group (Model)

#pragma mark - Detect duplicates

/** Verify that this item doesn't exist yet (another section with the same name). */- (BOOL) isDuplicated { BOOL duplicated = NO; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Group"];

// Set the predicate to find if another one exists. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", self.name];

NSError *error = nil; NSUInteger sections = [self.managedObjectContext countForFetchRequest:fetchRequest error:&error]; // The first one is the one this one. if (sections > 1) { duplicated = YES; }

return duplicated;}

@end

viernes, 8 de marzo de 13

Pruebasviernes, 8 de marzo de 13

La prueba del 3★ Incluir pruebas:๏ Más exhaustivo y sistemático.

๏ Refactorizar con mucho menos riesgo.

๏ Verificar la resolución de bugs y evitar regresiones.

★ Lo que proporciona el sistema (frameworks) no se prueba.

★ Como mínimo:๏ Modelo de negocio → Métodos añadidos

12

viernes, 8 de marzo de 13

Implementación de las pruebas★ OCUnit para no compicarse la vida★ Nombre explicativo★ Cobertura★ Casos relevantes★ Importante para Core Data: setUp y

tearDown๏ Carga del modelo

๏ Preparación del Persistent Store en memoria.

13

viernes, 8 de marzo de 13

Preparación★ - (void) setUp {

[super setUp]; // Create the Core Data stack. NSBundle *bundle = [NSBundle bundleForClass:[self class]]; model = [NSManagedObjectModel mergedModelFromBundles:@[bundle]]; coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; store = [coordinator addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; context = [[NSManagedObjectContext alloc] init]; [context setPersistentStoreCoordinator:coordinator];

// Instantiate three products for the tests. mainGroup = [NSEntityDescription insertNewObjectForEntityForName:@"Group" inManagedObjectContext:context]; mainGroup.name = @"A cool group";}

- (void) tearDown { context = nil; store = nil; coordinator = nil; model = nil;

[super tearDown];}

14

viernes, 8 de marzo de 13

No duplicación

★ #pragma mark - Detect duplication

- (void) testDuplicatedIsDetectedWhenTwoGroupsWithSameName { Group *anotherGroup = [NSEntityDescription insertNewObjectForEntityForName:@"Group" inManagedObjectContext:context]; anotherGroup.name = @"A cool group"; STAssertTrue([mainGroup isDuplicated], @"If another group exists with the same name it is considered a duplicate");}

- (void) testDuplicatedIsNotDetectedWhenTwoGroupsWithDifferentName { Store *anotherStore = [NSEntityDescription insertNewObjectForEntityForName:@"Group" inManagedObjectContext:context]; anotherGroup.name = @"Another cool group"; STAssertFalse([mainGroup isDuplicated], @"If no other group exists with a different name it is not considered a duplicate");}

15

viernes, 8 de marzo de 13

Desacoplamientoviernes, 8 de marzo de 13

This is an open discussion

viernes, 8 de marzo de 13

Marco Arment

“It’s so simple, I’ll just use plist”

viernes, 8 de marzo de 13

El problema★ Acoplamiento fuerte entre los

componentes de la aplicación (Models, Views, Controllers)

★ Difícil hacer mocks para aplicar TDD๏ TDD = componentes independientes

que se testan de forma independiente entre sí

๏ (¿Mocking de NSManagedObjectContext?)

19

viernes, 8 de marzo de 13

El problema★ Para testear un View Controller que

muestra en pantalla un listado de Meetings tendría que:๏ Crear un NSManagedObjectContext

๏ Cargar datos de prueba en la base de datos

๏ Hacer una búsqueda sobre los datos de prueba

๏ Generar un NSFetchedResultsController

๏ ...y finalmente testear

20

viernes, 8 de marzo de 13

El problema★ ...y después de hacer todo

esto, resulta que queremos hacer una versión para Android y compartir datos usando Parse/RoR/PHP/DynamoDB

★ ...o prefieres usar plists para no complicarte la vida (como Marco Arment, pero al revés)

21

viernes, 8 de marzo de 13

El problema

http://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years

viernes, 8 de marzo de 13

“The database is a DETAIL of our application”

viernes, 8 de marzo de 13

View Controller

Model(Core Data)

View

Meeting.h

viernes, 8 de marzo de 13

Como la base de datos es un detalle, la podemos quitar

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

Entidad: Objeto que contiene las reglas de negocio genéricas, aquellas que se pueden aplicar siempre en cualquier contexto.

Por ejemplo: Si a un evento se inscriben tres personas, este queda automáticamente confirmado

MeetingEntity

GroupEntity

AttendeeEntity

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

Interactor: Contiene reglas de negocio específicas

Por ejemplo: Dos eventos no pueden tener el mismo nombre

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

viernes, 8 de marzo de 13

¿Y la base de datos?

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

Request Object

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

Request Object

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

Request Object

...accede a Core Data y busca los objetos que cumplen los criterios del RequestObject

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

Entidades

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

Entidades

ResponseObject

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsGateway

Model(Core Data)

ResponseObject

View

viernes, 8 de marzo de 13

show me the code!!

viernes, 8 de marzo de 13

Duck Typing

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsParseGateway

Model(Core Data)

ResponseObject

View

RequestObjectProtocol

ResponseObject

viernes, 8 de marzo de 13

QueryRequestProtocol

@protocol QueryRequestProtocol

@required@property (nonatomic, strong) NSString *queryString;@property (nonatomic, strong, readonly) NSDictionary *components;

@end

viernes, 8 de marzo de 13

BrowseMeetingsInteractor

@interface BrowseMeetingsInteractor : NSObject

@property (nonatomic, strong) id<MeetingGatewayProtocol> gateway;

- (id<StandardResponseProtocol>) getResponseForRequest:(id<QueryRequestProtocol >)request;@end

viernes, 8 de marzo de 13

MeetingGateway

@class MeetingEntity;

@protocol MeetingGatewayProtocol <NSObject>

@required- (id)processRequest:(id<QueryRequestProtocol> *)request;- (void)save:(MeetingEntity *)meeting error:(NSError **)error;@end

viernes, 8 de marzo de 13

viewDidLoad if (!self.fridgeResponse) { IFCoreDataFridgeGateway *gateway = [[IFCoreDataFridgeGateway alloc] init]; gateway.shouldReturnFetchResultsController = false; gateway.context = self.context;

self.fridgeInteractor = [[IFViewFridgeInteractor alloc] init]; self.fridgeInteractor.gateway = gateway; self.fridgeResponse = [self.fridgeInteractor getResponseForRequest:self.request]; } if (!self.fridgeItemsResponse) { IFCoreDataFridgeItemGateway *fridgeItemGateway = [[IFCoreDataFridgeItemGateway alloc] init]; fridgeItemGateway.context = self.context; fridgeItemGateway.shouldReturnFetchResultsController = YES; self.fridgeItemsInteractor = [[IFBrowseFridgeItemsInteractor alloc] init]; self.fridgeItemsInteractor.gateway = fridgeItemGateway; IFFridgeItemRequestObject *fridgeItemsRequest = [[IFFridgeItemRequestObject alloc] init]; ; fridgeItemsRequest.queryString = [NSString stringWithFormat:@"fridge-slug=%@",self.request.components[@"slug"] ] ; self.fridgeItemsResponse = [self.fridgeItemsInteractor getResponseForRequest:fridgeItemsRequest]; }

viernes, 8 de marzo de 13

¿Cómo cambiamos de CoreData a Parse?

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsParseGateway

Cache(Core Data)

ResponseObject

View

Parse

Mientras la clase MeetingsParseGateway y

MeetingsGateway cumplan el mismo protocolo ¡todo

funciona!

RequestObject

ResponseObject

viernes, 8 de marzo de 13

¡¡Podemos testearlo todo!!

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsParseGateway

Cache(Core Data)

ResponseObject

View

Parse

RequestObject

ResponseObject

viernes, 8 de marzo de 13

View Controller

MockBrowseMeetingsInteractor

MockResponseObject

RequestObject

Para testear el view controller, basta con tener un Mock del

Interactor

viernes, 8 de marzo de 13

View Controller

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MeetingsParseGateway

Cache(Core Data)

ResponseObject

View

Parse

Mientras la clase MeetingsParseGateway y

MeetingsGateway cumplan el mismo protocolo ¡todo

funciona!

RequestObject

ResponseObject

viernes, 8 de marzo de 13

Request:quiero ver todos los

objetos Meeting

MeetingEntity

GroupEntity

AttendeeEntity

BrowseMeetingsInteractor

MockGateway

MockRequestObjectResponseObject

viernes, 8 de marzo de 13

En la vida nada es gratis...★Hay que escribir más código

★Hay que pensar en interfaces (protocolos)

★¿Rendimiento?

51

viernes, 8 de marzo de 13

¿Qué pensáis?

52

viernes, 8 de marzo de 13

¿Qué pensáis?★ ¿Creéis que tener un sistema de componentes

realmente desacoplados es un buen sistema?

52

viernes, 8 de marzo de 13

¿Qué pensáis?★ ¿Creéis que tener un sistema de componentes

realmente desacoplados es un buen sistema?★ ¿Pensáis que tener un sistema en el que puedo

testear sus componentes por separado, es un buen sistema?

52

viernes, 8 de marzo de 13

¿Qué pensáis?★ ¿Creéis que tener un sistema de componentes

realmente desacoplados es un buen sistema?★ ¿Pensáis que tener un sistema en el que puedo

testear sus componentes por separado, es un buen sistema?

★ ¿Pensáis que tener un sistema flexible en el que puedo posponer las decisiones sobre aspectos fundamentales del mismo hasta que realmente las necesito es un buen sistema?

52

viernes, 8 de marzo de 13

¿Qué pensáis?★ ¿Creéis que tener un sistema de componentes

realmente desacoplados es un buen sistema?★ ¿Pensáis que tener un sistema en el que puedo

testear sus componentes por separado, es un buen sistema?

★ ¿Pensáis que tener un sistema flexible en el que puedo posponer las decisiones sobre aspectos fundamentales del mismo hasta que realmente las necesito es un buen sistema?

★ ¿Pensáis que poder sustituir unas componentes por otras es un buen sistema?

52

viernes, 8 de marzo de 13

¿Qué pensáis?★ ¿Creéis que tener un sistema de componentes

realmente desacoplados es un buen sistema?★ ¿Pensáis que tener un sistema en el que puedo

testear sus componentes por separado, es un buen sistema?

★ ¿Pensáis que tener un sistema flexible en el que puedo posponer las decisiones sobre aspectos fundamentales del mismo hasta que realmente las necesito es un buen sistema?

★ ¿Pensáis que poder sustituir unas componentes por otras es un buen sistema?

★ ¿Pensáis que un sistema en el que una buena parte del código se puede autogenerar o reutilizar es un buen sistema?

52

viernes, 8 de marzo de 13

Cuando no... ★Si quieres hacer un prototipo rápido de la applicación

★Si no quieres hacer TDD, ni desacoplar componentes ni dormir más tranquilo por las noches cuando tu app se la descarguen miles de personas

53

viernes, 8 de marzo de 13

¡Es una inversión de futuro!

¡Gracias!

viernes, 8 de marzo de 13