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