¿Cómo puedo hacer que OCMock bajo ARC deje de anular un conjunto de subclase NSProxy usando una propiedad débil?

Debajo ARC, Tengo un objeto, Child eso tiene un weak propiedad, parent. Estoy tratando de escribir algunas pruebas para Child, y me estoy burlando de su parent propiedad usando OCMock.

Bajo ARC, estableciendo un NSProxy la subclase que usa un establecedor de propiedad débil sintetizado no establece la propiedad ... la línea después de que se establece la propiedad débil, verificarla revela que ya está 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 solucionar esto usando un assign propiedad en lugar de una weak propiedad para el Child para hacer referencia al Parent, pero luego tendría que hacerlo nil la parent cuando terminé con eso (como una especie de hombre de las cavernas), que es exactamente el tipo de cosas que se suponía que ARC debía obviar.

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

Editar: Parece tener que ver con OCMockObject siendo un NSProxy, si hago aParent ser una instancia de NSObject,la child.parent la referencia débil "tiene" un valor no nulo. Todavía estoy buscando una manera 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 cambió condicionalmente mis propiedades de débil -> asignar. Su experiencia puede ser diferente:

#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

preguntado el 01 de febrero de 12 a las 22:02

Mira este mensaje de confirmación: github.com/erikdoe/ocmock/commit/… Parece que el equipo de OCMock está investigando lo mismo. -

3 Respuestas

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

respondido 12 mar '12, 22:03

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

Escribí una clase simple que extiende NSObject, no NSProxy, que reenvía todas las invocaciones de 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

¡Simplemente use el objeto resultante como lo haría con un OCMockObject normal!

Respondido 27 Jul 12, 22:07

Seguro. Está yendo nil porque inmediatamente después de asignar child.parent, su objeto proxy en sí es liberado por su prueba (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 hacer esto trivialmente insertando una llamada a

[aParent self];

al final de su método. Esa llamada de función no hace nada (-self solo regresa self), pero se asegurará de que ARC mantenga vivo el objeto.

Una alternativa sería cambiar su declaración de aParent para ser __autoreleasing, lo que hace que se comporte más como MRR en el sentido de que ARC simplemente dejará una referencia liberada automáticamente en ese espacio en lugar de liberar explícitamente el objeto cuando la variable salga del alcance. Puedes hacer eso con

__autoreleasing OCMockObject *aParent = ...

Dicho esto, la primera solución probablemente sea más limpia, porque explícitamente mantendrá vivo el objeto durante la prueba.

Respondido 02 Feb 12, 03:02

Agregué la referencia que sugirió al final de mi prueba y verifiqué que no funciona. parent sigue siendo válido al final de la prueba, pero child.parent es siempre nil. - perro de la pradera

@Prairiedogg: ¿Quizás 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 la retención / liberación). - Lily Ballard

Adición __autoreleasing también parece no funcionar. Puedo verificar que la referencia a aParent en el alcance de la prueba es válido hasta la última línea del método de prueba, pero el sintetizado child la propiedad nunca se establece, siempre nil. - perro de la pradera

@Prairiedogg: intente cambiarlo a assign. Si eso funciona, implica que OCMockObject simplemente no admite referencias débiles. Si eso también falla, definitivamente tienes un problema. Estás seguro que child en sí mismo es no nulo? - Lily Ballard

Claro, ya lo verifiqué assign funciona (mencionado en la pregunta original). Verifiqué eso child no es nulo. También verifiqué que cambiando aParent a una instancia de NSObject hace child.parent "mantener" correctamente. - perro de la pradera

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.