10

Bajo ARC, tengo un objeto, Child que tiene una propiedad weak, parent. Estoy intentando escribir algunas pruebas para Child, y me estoy burlando de su propiedad parent usando OCMock.¿Cómo puedo obtener OCMock en ARC para dejar de registrar un conjunto de subclase NSProxy utilizando una propiedad débil?

Bajo ARC, el establecimiento de una subclase NSProxy usando un regulador débil propiedad sintetizada no establecer la propiedad ... la línea después de la propiedad débil se establece, la comprobación revela que ya es nil. Aquí está el ejemplo concreto:

@interface Child : NSObject 
@property (nonatomic, weak) id <ParentInterface>parent; 
@end 

@implementation Child 
@synthesize parent = parent_; 
@end 

// ... later, inside a test class ... 

- (void)testParentExists 
{ 
    // `mockForProtocol` returns an `NSProxy` subclass 
    // 
    OCMockObject *aParent = [OCMockObject mockForProtocol:@protocol(ParentInterface)]; 
    assertThat(aParent, notNilValue()); 

    // `Child` is the class under test 
    // 
    Child *child = [[Child alloc] init]; 
    assertThat(child, notNilValue()); 

    assertThat(child.parent, nilValue()); 
    child.parent = (id<ParentInterface>)aParent; 
    assertThat([child parent], notNilValue()); // <-- This assertion fails 
    [aParent self]; // <-- Added this reference just to ensure `aParent` was valid until the end of the test. 
} 

Sé que puedo evitar esto usando una propiedad assign en lugar de un weak propiedad para el Child hacer referencia a la Parent, pero entonces tendría que nil la parent cuando estaba hecho con eso (como una especie de hombre de las cavernas), que es exactamente el tipo de cosa que se suponía que ARC obviaba.

¿Alguna sugerencia sobre cómo hacer que esta prueba pase sin cambiar el código de mi aplicación?

Editar: Parece que tiene que ver con OCMockObject ser un NSProxy, si hago aParent ser una instancia de NSObject, la child.parent débil de referencia "tiene" un valor no nulo. Todavía estoy buscando una forma de hacer que esta prueba pase sin cambiar el código de la aplicación.

Editar 2: Después de aceptar la respuesta de Blake, hice una implementación en mi proyecto de una macro de preprocesador que condicionalmente cambió mis propiedades de débil -> asignar. Su millaje puede variar:

#if __has_feature(objc_arc) 
#define BBE_WEAK_PROPERTY(type, name) @property (weak, nonatomic) type name 
#else 
#define BBE_WEAK_PROPERTY(type, name) @property (assign, nonatomic) type name 
#endif 
+0

Consulte este mensaje de confirmación: https://github.com/erikdoe/ocmock/commit/dbdb233ae84498077f7e946abb49731968333f0b Parece que el equipo de OCMock está investigando lo mismo. – Don

Respuesta

9

Hemos estado luchando con este mismo problema y, de hecho, tiene que ver con una incompatibilidad entre ARC y referencias débiles a los objetos derivados NSProxy. Recomendaría usar una directiva de preprocesador para compilar condicionalmente las referencias de delegado débiles para asignar dentro del conjunto de pruebas para que pueda probarlas a través de OCMock.

0

Sure. Va nil porque inmediatamente después de asignar child.parent, la prueba arroja su propio objeto proxy (ya que ya no se hace referencia), y esto hace que la referencia débil sea nula. Entonces, la solución es mantener vivo su objeto proxy durante la prueba. Puede hacerlo de manera trivial insertando una llamada al

[aParent self]; 

al final de su método. Esa llamada de función no hace nada (-self simplemente devuelve self), pero asegurará que ARC conserve el objeto con vida.

Una alternativa sería la de cambiar su declaración de aParent ser __autoreleasing, lo que hace que se comporte más como MRR en ese arco se acaba de salir de una referencia autoreleased en esa ranura en lugar de liberar explícitamente el objeto cuando la variable se sale del ámbito . Usted puede hacer eso con

__autoreleasing OCMockObject *aParent = ... 

Dicho esto, la primera solución es, probablemente, más limpio, ya que está manteniendo explícitamente el objeto vivo durante la prueba.

+0

Agregué la referencia que sugirió en la parte inferior de mi prueba y he verificado que no funciona. 'parent' sigue siendo válido en la parte inferior de la prueba, pero' child.parent' siempre es 'nil'. – Prairiedogg

+0

@Prairiedogg: ¿Tal vez su prueba está funcionando como se esperaba, entonces? O eso o OCMock no es compatible con referencias débiles (lo que puede suceder si anula retención/liberación). –

+0

Agregar '__autoreleasing' también parece no funcionar. Puedo verificar que la referencia a 'aParent' en el alcance de la prueba sea válida hasta la última línea del método de prueba, pero la propiedad' child' sintetizada nunca se establece, siempre es 'nil'. – Prairiedogg

6

Encontré una solución diferente a una macro condicional ya que estaba probando un código para el que no pude cambiar el código.

Escribí una clase simple que amplía NSObject, no NSProxy, que reenvía todas las invocaciones del selector al OCMockProxy.

CCWeakMockProxy.h:

#import <Foundation/Foundation.h> 

/** 
* This class is a hack around the fact that ARC weak references are immediately nil'd if the referent is an NSProxy 
* See: http://stackoverflow.com/questions/9104544/how-can-i-get-ocmock-under-arc-to-stop-nilling-an-nsproxy-subclass-set-using-a-w 
*/ 
@interface CCWeakMockProxy : NSObject 

@property (strong, nonatomic) id mock; 

- (id)initWithMock:(id)mockObj; 

+ (id)mockForClass:(Class)aClass; 
+ (id)mockForProtocol:(Protocol *)aProtocol; 
+ (id)niceMockForClass:(Class)aClass; 
+ (id)niceMockForProtocol:(Protocol *)aProtocol; 
+ (id)observerMock; 
+ (id)partialMockForObject:(NSObject *)anObject; 

@end 

CCWeakMockProxy.m:

#import "CCWeakMockProxy.h" 
#import <OCMock/OCMock.h> 


#pragma mark Implementation 
@implementation CCWeakMockProxy 

#pragma mark Properties 
@synthesize mock; 

#pragma mark Memory Management 
- (id)initWithMock:(id)mockObj { 
    if (self = [super init]) { 
     self.mock = mockObj; 
    } 
    return self; 
} 

#pragma mark NSObject 
- (id)forwardingTargetForSelector:(SEL)aSelector { 
    return self.mock; 
} 

- (BOOL)respondsToSelector:(SEL)aSelector { 
    return [self.mock respondsToSelector:aSelector]; 
} 

#pragma mark Public Methods 
+ (id)mockForClass:(Class)aClass { 
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject mockForClass:aClass]]; 
} 

+ (id)mockForProtocol:(Protocol *)aProtocol { 
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject mockForProtocol:aProtocol]]; 
} 

+ (id)niceMockForClass:(Class)aClass { 
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject niceMockForClass:aClass]]; 
} 

+ (id)niceMockForProtocol:(Protocol *)aProtocol { 
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject niceMockForProtocol:aProtocol]]; 
} 

+ (id)observerMock { 
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject observerMock]]; 
} 

+ (id)partialMockForObject:(NSObject *)anObject { 
    return [[CCWeakMockProxy alloc] initWithMock:[OCMockObject partialMockForObject:anObject]]; 
} 

@end 

sólo tiene que utilizar el objeto resultante como si fuera un OCMockObject regular!

Cuestiones relacionadas