Модульный тест iOS: как установить/обновить/проверить firstResponder?

Как вы пишете модульные тесты первого ответчика?

Я пытаюсь написать тест, чтобы подтвердить, что метод перемещает фокус на следующее текстовое поле. controller является потомком UIViewController. Но этот исследовательский тест терпит неудачу:

- (void)testFirstResponder
{
    [controller view];
    [[controller firstTextField] becomeFirstResponder];

    STAssertTrue([[controller firstTextField] isFirstResponder], nil);
}

Первая строка вызывает загрузку представления, чтобы его выходы были на месте. Текстовые поля не равны нулю. Но испытание никогда не проходит.

Я предполагаю, что becomeFirstResponder не устанавливает первого ответчика сразу, а назначает его позже. Итак, есть ли хороший способ написать модульный тест против него?

Вытягивание ответа из комментария в принятом ответе… Ненадолго дайте поработать:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];

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


person Jon Reid    schedule 21.05.2011    source источник


Ответы (3)


Я предполагаю, что управление/изменение цепочки первого респондента каким-то образом выполняется в основном цикле, когда пользовательский интерфейс обновляется, готовясь к следующей обработке события. Если эта гипотеза верна, я бы просто сделал следующее:

-(void)assertIfNotFirstResponder:(UITextField*)field {
    STAssertTrue([field isFirstResponder], nil);
}

- (void)testFirstResponder
{
     [controller view];
     [[controller firstTextField] becomeFirstResponder];
     [self performSelector:@selector(@"assertIfNotFirstResponder:") withObject:[controller firstTextField] afterDelay:0.0];
 }

Примечание. Я использовал задержку 0,0, потому что просто хочу, чтобы сообщение было помещено в очередь событий и отправлено как можно скорее. Мне нужен просто способ вернуться к основному циклу для его обслуживания. Это не должно привести к фактической задержке в вашем случае. Если вы выполняете несколько тестов одного и того же типа, т. е. многократно меняете элемент управления, который отвечает первым, этот метод должен гарантировать, что все эти события правильно упорядочены с событиями, сгенерированными performSelector.

Если вы запускаете свои тесты из другого потока, вы можете использовать – performSelectorOnMainThread:withObject:waitUntilDone:

person sergio    schedule 28.05.2011
comment
Хорошая мысль, но для модульного тестирования размещение сообщения в очереди событий не работает, потому что тест завершается, а фикстура разрывается до утверждения. Контрпример: изменение STAssertTrue на STAssertFalse не имеет значения. - person Jon Reid; 30.05.2011
comment
Правильно, это не сработает... в качестве дополнительной подсказки я только что узнал, что вы можете запустить цикл выполнения на некоторое время перед проверкой условия: [[NSRunLoop currentRunLoop] runUntilDate:fiveSecondsFromNow]; (от: stackoverflow.com/questions/1077737/) . Я не совсем уверен, что это решение действительно элегантно, но оно должно работать и может подойти для небольших случаев (когда вам не нужно идти с Фрэнком)... - person sergio; 30.05.2011
comment
Превосходно! Даже указание времени выполнения, равного нулю, помогает. - person Jon Reid; 31.05.2011

Используя Xcode 5.1 и XCTestCase, это работает нормально:

- (void)testFirstResponder
{      
  // Make sure the controller's view has a window
  UIWindow *window = [[UIWindow alloc] init];
  [window addSubview:controller.view];

  // Call whatever method you're testing
  [controller.textView becomeFirstResponder];

  // Assert that the desired subview is the first responder
  XCTAssertTrue([sut.textView isFirstResponder]);
}

Чтобы представление/подпредставление стало первым ответчиком, оно должно быть частью иерархии представлений, а это означает, что свойство окна корневого представления должно< /em>.

Джон и Серджио упоминают, что вам может понадобиться вызвать [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]] после вызова becomeFirstResponder в желаемом подпредставлении, но я обнаружил, что в нашем случае это не требуется.

Однако ваш пробег может варьироваться (даже в зависимости от версии Xcode, которую вы используете), поэтому вам может понадобиться или не понадобиться включать такой вызов.

person JRG-Developer    schedule 11.06.2014
comment
Вы находите, что это работает, не делая окно ключевым окном? - person bejonbee; 18.08.2015
comment
Начиная с iOS 10.3 это работает, не делая его ключевым окном. - person Xavier Lowmiller; 13.07.2017

Вам необходимо убедиться, что textField установлен в иерархии представлений.

Если свойство окна представления содержит объект UIWindow, оно было установлено в иерархии представлений; если он возвращает nil, представление отсоединяется от какой-либо иерархии.

Надеюсь, это поможет....

person adam    schedule 28.05.2011
comment
Вы правы, UIWindow был нулевым. Есть ли способ изменить это в модульном тесте? Нажатие контроллера представления не работает, потому что оно планирует вещи, но (как отмечено в моем ответе @sergio) недостаточно скоро для модульного теста. - person Jon Reid; 30.05.2011
comment
Теперь у меня есть ответ. +1 за заполнение части головоломки. - person Jon Reid; 31.05.2011