Как я могу получить OCMock под ARC, чтобы прекратить обнуление набора подклассов NSProxy, используя слабое свойство? - PullRequest
10 голосов
/ 02 февраля 2012

Под 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 когда я закончил с этим (вроде какого-то пещерного человека), это именно то, что АРК должен был избежать.

Любые предложения о том, как пройти этот тест без изменения кода моего приложения?

Редактировать : Кажется, это связано с 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 ]

9 голосов
/ 12 марта 2012

Мы боролись с этой же проблемой, и это действительно связано с несовместимостью между ARC и слабыми ссылками на объекты, производные от NSProxy.Я бы порекомендовал использовать директиву препроцессора для условной компиляции ваших слабых ссылок делегатов для назначения в наборе тестов, чтобы вы могли проверить их через OCMock.

6 голосов
/ 27 июля 2012

Я нашел решение, отличное от условного макроса, так как я тестировал код, для которого я не мог изменить код.

Я написал простой класс, который расширяет 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: /4282713/kak-ya-mogu-poluchit-ocmock-pod-arc-chtoby-prekratit-obnulenie-nabora-podklassov-nsproxy-ispolzuya-slaboe-svoistvo
 */
@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!

0 голосов
/ 02 февраля 2012

Конечно.Это происходит nil, потому что сразу после назначения child.parent ваш прокси-объект сам по себе освобождается вашим тестом (так как на него больше нет ссылок), и это приводит к тому, что слабая ссылка обнуляется.Таким образом, решение состоит в том, чтобы сохранить ваш прокси-объект живым во время теста.Вы можете сделать это тривиально, вставив вызов в

[aParent self];

в конце вашего метода.Этот вызов функции ничего не делает (-self просто возвращает self), но он гарантирует, что ARC поддерживает объект живым.

В качестве альтернативы можно изменить объявление aParent на __autoreleasing, что делает его более похожим на MRR в том смысле, что ARC просто оставит автоматически освобожденную ссылку в этом слоте вместо явного освобождения объекта, когда переменная выходит из области видимости.Вы можете сделать это с помощью

__autoreleasing OCMockObject *aParent = ...

Тем не менее, первое решение, вероятно, чище, потому что вы явно поддерживаете объект живым во время теста.

...