TyphoonPatcher для мокинга в юнит-тестах

У меня есть Assembly:

@interface MDUIAssembly : TyphoonAssembly

@property (nonatomic, strong, readonly) MDServiceAssembly *services;
@property (nonatomic, strong, readonly) MDModelAssembly *models;

- (id)choiceController;

@end

@implementation MDUIAssembly

- (void)resolveCollaboratingAssemblies
{
    _services = [TyphoonCollaboratingAssemblyProxy proxy];
    _models = [TyphoonCollaboratingAssemblyProxy proxy];
}

- (id)choiceController
{
    return [TyphoonDefinition withClass:[MDChoiceViewController class]
                          configuration: ^(TyphoonDefinition *definition) {
        [definition useInitializer:@selector(initWithAnalytics:diary:)
                        parameters: ^(TyphoonMethod *initializer) {
            [initializer injectParameterWith:[_services analytics]];
            [initializer injectParameterWith:[_models diary]];
        }];
    }];
}

@end

Вот что я пытаюсь сделать в тестах:

- (void)setUp
{
    patcher = [TyphoonPatcher new];
    MDUIAssembly *ui = (id) [TyphoonComponentFactory defaultFactory];
    [patcher patchDefinition:[ui choiceController] withObject:^id{
       return mock([MDChoiceViewController class]);
    }];
    [[TyphoonComponentFactory defaultFactory] attachPostProcessor:patcher];
}

- (void) tearDown 
{
   [super tearDown];
   [patcher rollback];
}

К сожалению, мой setUp не работает со следующим сообщением:

-[MDChoiceViewController key]: unrecognized selector sent to instance 0xbb8aaf0

Что я делаю неправильно?


person Eugen Martynov    schedule 25.07.2014    source источник
comment
мы будем признательны за ваш вклад в: github.com/typhoon-framework/Typhoon/issues/ 240   -  person Jasper Blues    schedule 26.07.2014


Ответы (2)


Вот несколько дополнительных советов по поводу основного ответа. . .

Модульные тесты и интеграционные тесты:

В Typhoon мы придерживаемся традиционных терминов:

  • Модульные тесты: тестирование вашего класса отдельно от соавторов. Здесь вы вводите дубликаты тестов, такие как макеты или заглушки, вместо всех реальных зависимостей.

  • Интеграционные тесты: тестирование вашего класса с участием реальных соавторов. Хотя вы можете пропатчить наш компонент, чтобы привести систему в состояние, необходимое для этого теста.

Таким образом, любой тест, использующий TyphoonPatcher, вероятно, будет интеграционным тестом.

Подробнее здесь: Тестирование интеграции с Typhoon

Разрешение совместных сборок:

Это требовалось в более ранней версии Typhoon, но больше не требуется. Любые свойства, являющиеся подклассом TyphoonAssembly, будут рассматриваться как взаимодействующие сборки. Удалите следующее:

- (void)resolveCollaboratingAssemblies
{
    _services = [TyphoonCollaboratingAssemblyProxy proxy];
    _models = [TyphoonCollaboratingAssemblyProxy proxy];
}

Тесты создают свою собственную сборку:

Мы рекомендуем, чтобы тесты создавали экземпляры и удаляли их на TyphoonComponentFactory. Преимущества:

  • [TyphoonComponentFactory defaultFactory] является глобальным и имеет некоторые недостатки.
  • Интеграционные тесты могут определять свои собственные исправления, не беспокоясь о возвращении системы в исходное состояние.
  • Помимо использования TyphoonPatcher, при желании вы можете создать сборку, в которой определения некоторых компонентов переопределены.
person Jasper Blues    schedule 25.07.2014
comment
Я передаю почти все зависимости из параметров инициализации, но в одном месте я вызываю фабрику Typhoon, чтобы продолжить. Вероятно, мне следует передать контроллер или лучшую сборку через init, но я думаю, что мой init уже имеет 5 зависимостей. Это хороший запах, что класс делает слишком много, но нет простого/четкого разделения. Я обязательно постараюсь упростить код. Как по мне, писать сложный код obj-c гораздо проще, чем java - person Eugen Martynov; 26.07.2014
comment
Это может показаться неприятным, но иногда классу верхнего уровня (например, контроллеру) необходимо организовать множество компонентов. Было бы неплохо создать объект, объединяющий нескольких соавторов, и передать его. в качестве альтернативы, чтобы избежать длинного метода инициализации, вы можете использовать установщики свойств или внедрение метода (ну, в любом случае, это варианты). Вы можете переопределить -(void)typhoonDidInject, который вызывается после инъекции, чтобы использовать его так же, как и в случае с init - утвердите требуемое состояние перед продолжением. - person Jasper Blues; 26.07.2014
comment
Я решил пойти с этим подходом - person Eugen Martynov; 28.07.2014

Вы столкнулись с плохим выбором дизайна со стороны Typhoon, но есть простой обходной путь.

Вы используете этот метод:

[patcher patchDefinition:[ui choiceController] withObject:^id{
   return mock([MDChoiceViewController class]);
}];

. . который ожидает TyphoonDefinition в качестве аргумента. При загрузке Тайфуна:

  • Мы начинаем с одного или более TyphoonAssembly подклассов, которые Тайфун использует для получения рецептов для строительных компонентов. Затем эти TyphoonAssembly подклассы отбрасываются.
  • Теперь у нас есть TyphoonComponentFactory, который позволит любому из ваших TyphoonAssembly интерфейсов позировать перед ним. (Это сделано для того, чтобы вы могли иметь несколько конфигураций одного и того же класса, избегая при этом магических строк, разрешая автозаполнение в вашей среде IDE и т. д.).

Когда TyphoonPatcher был написан, он был разработан для случая, когда вы получаете новый TyphoonComponentFactory для своих тестов (рекомендуется), например:

//This is an actual TyphoonAssembly not the factory posing as an assembly
MiddleAgesAssembly* assembly = [MiddleAgesAssembly assembly];

TyphoonComponentFactory* factory = [TyphoonBlockComponentFactory factoryWithAssembly:assembly];

TyphoonPatcher* patcher = [[TyphoonPatcher alloc] init];
[patcher patchDefinition:[assembly knight] withObject:^id
{
    Knight* mockKnight = mock([Knight class]);
    [given([mockKnight favoriteDamsels]) willReturn:@[
        @"Mary",
        @"Janezzz"
    ]];

    return mockKnight;
}];

[factory attachPostProcessor:patcher];
Knight* knight = [(MiddleAgesAssembly*) factory knight];

Что произошло:

Итак, проблема в том, что TyphoonPatcher ожидает TyphoonDefinition от TyphoonAssembly, а вместо этого получает фактический компонент от TyphoonComponentFactory.

Очень запутанно, и этот способ получения патчера должен быть объявлен устаревшим.

Решение:

Вместо этого используйте следующее:

[patcher patchDefinitionWithSelector:@selector(myController) withObject:^id{
     return myFakeController;
}];
person Jasper Blues    schedule 25.07.2014
comment
Спасибо за объяснение и обходной путь. Я видел в отладке, что нет упоминания об определениях, но я не пришел к такому простому выводу. Я должен больше читать код. - person Eugen Martynov; 26.07.2014
comment
У ряда других была такая же проблема, и теперь ясно, что API приводит к путанице и должен быть исправлен. Так что спасибо, что поделились своим опытом. - person Jasper Blues; 26.07.2014
comment
Джаспер, в tearDown не получается. _factory равен нулю в постпроцессоре. - person Eugen Martynov; 28.07.2014
comment
Похоже, что если патчер не используется, то фабрика равна нулю. Кроме того, мои следующие тесты, использующие factory, начали давать сбой после отключения. Похоже, некоторые проблемы с NSNotificationCenter. расследование - person Eugen Martynov; 28.07.2014
comment
Если каждый интеграционный тест создает свой собственный патчер Typhoon, отсоединяться не нужно. А пока не могли бы вы создать отчет об ошибке (GitHub) для этого? - person Jasper Blues; 29.07.2014
comment
Я не уверен, что именно создавать тикет - про исключение, если патчер не используется или про то, что не надо вызывать deattach? - person Eugen Martynov; 29.07.2014
comment
Переменная factory выше не используется. Почему? - person fatuhoku; 04.08.2014
comment
@fatuhoku Я завершил пример, используя factory. - person Jasper Blues; 06.08.2014