Как я могу заставить OCMock в ARC перестать заполнять набор подклассов NSProxy с использованием слабого свойства?
В ARC
у меня есть объект Child
, у которого есть свойство weak
, parent
. Я пытаюсь написать несколько тестов для Child
, и я издеваюсь над свойством parent
, используя OCMock
.
В ARC установка подкласса NSProxy
с использованием синтезированного слабого свойства setter не устанавливает свойство... строка после свойства weak задана, проверяя, что она уже nil
. Вот конкретный пример:
@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.
}
Я знаю, что обойти это можно с помощью свойства assign
вместо свойства weak
для Child
для ссылки на parent
, но тогда мне нужно nil
выйти из parent
когда я закончил с ним (как какой-то пещерный человек), что именно то, что ARC должно было устранить.
Любые предложения о том, как пройти этот тест, не меняя код приложения?
Изменить: похоже, что OCMockObject
является NSProxy
, если я делаю aParent
экземпляр NSObject
, слабая ссылка "" значение, отличное от нуля". Все еще ищете способ пройти этот тест без изменения кода приложения.
Изменить 2: после принятия ответа Блейка я выполнил реализацию в моем проекте макроса препроцессора, который условно изменил мои свойства из weak → assign. Ваш пробег может отличаться:
#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
Ответы
Ответ 1
Мы боролись с этой же проблемой, и она действительно связана с несовместимостью между ARC и слабыми ссылками на объекты, созданные из NSProxy. Я бы рекомендовал использовать предпроцессорную директиву, чтобы условно скомпилировать ваши слабые ссылки делегатов для назначения внутри набора тестов, чтобы вы могли протестировать их через OCMock.
Ответ 2
Я нашел другое решение, чем условный макрос, так как я тестировал код, который я не мог изменить для кода.
Я написал простой класс, который расширяет NSObject, а не NSProxy, который перенаправляет все вызовы селектора на 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
Просто используйте результирующий объект, как обычный OCMockObject!
Ответ 3
Конечно. Это происходит nil
, потому что сразу после назначения child.parent
ваш прокси-объект сам выдается вашим тестом (поскольку он больше не ссылается), и это приводит к тому, что слабая ссылка на нуль. Таким образом, решение заключается в том, чтобы сохранить ваш прокси-объект в течение всего теста. Вы можете сделать это тривиально, вставив вызов
[aParent self];
в конце вашего метода. Этот вызов функции ничего не делает (-self
просто возвращает self
), но это гарантирует, что ARC сохраняет объект в живых.
Альтернативой было бы изменить объявление aParent
как __autoreleasing
, что делает его более похожим на MRR в том, что ARC просто оставит автореализованную ссылку в этом слоте вместо явного освобождения объекта при переходе переменной вне сферы действия. Вы можете сделать это с помощью
__autoreleasing OCMockObject *aParent = ...
Тем не менее, первое решение, вероятно, более чистое, потому что вы явно сохраняете объект в течение теста.