Можно ли протестировать IBAction?

Модульное тестирование IBOutlets довольно просто, но как насчет IBActions? Я пытался найти способ, как это сделать, но безуспешно. Есть ли способ установить модульное тестовое соединение между IBAction в контроллере представления и кнопкой в ​​файле пера?


person sash    schedule 09.09.2013    source источник
comment
Что вы подразумеваете под тестированием? Вы хотите сделать модульный тест для них? Если да, то просто инкапсулируйте свои действия в функции/классы и протестируйте эти классы с помощью модульных тестов. Если у вас есть что-то еще на уме - пожалуйста, объясните это немного подробнее.   -  person Grzegorz Krukowski    schedule 09.09.2013
comment
Да, я хочу написать модульный тест.   -  person sash    schedule 09.09.2013


Ответы (7)


Для полного модульного тестирования для каждого выхода/действия требуется три теста:

  1. Розетка подключена к виду?
  2. Связана ли розетка с действием, которое мы хотим?
  3. Вызовите действие напрямую, как если бы оно было запущено розеткой.

Я делаю это все время, чтобы TDD мои контроллеры представления. Вы можете увидеть пример в этом скринкасте.

Похоже, вы спрашиваете конкретно о втором шаге. Вот пример модульного теста, подтверждающего, что прикосновение внутри myButton вызовет действие doSomething: Вот как я выражаю это, используя OCHamcrest . (sut — тестовое приспособление для тестируемой системы.)

- (void)testMyButtonAction {
    assertThat([sut.myButton actionsForTarget:sut
                              forControlEvent:UIControlEventTouchUpInside],
               contains(@"doSomething:", nil));
}

В качестве альтернативы, вот версия без Hamcrest:

- (void)testMyButtonAction {
    NSArray *actions = [sut.myButton actionsForTarget:sut
                              forControlEvent:UIControlEventTouchUpInside];
    XCTAssertTrue([actions containsObject:@"doSomething:"]);
}
person Jon Reid    schedule 10.09.2013
comment
Я использую модульное тестирование, поэтому, как проверить, что эта кнопка нажата, используя как XCTAsertTrue и т. д. - person Vivek Yadav; 07.07.2014
comment
Я использую кнопку перехода, так как я могу проверить? - person Vivek Yadav; 07.07.2014
comment
Чтобы сделать это таким образом, вы должны создать IBOutlet специально для целей тестирования. Также вы должны ссылаться на обработчик в строке, что означает, что он сломается при рефакторинге. - person jowie; 08.12.2014
comment
@jowie: AppCode автоматически рефакторит такие вещи. Но даже без AppCode я готов заплатить небольшую цену из-за легкости и скорости, с которой я могу получить обратную связь. - person Jon Reid; 16.03.2016

Я сделал это с помощью OCMock, вот так:

MyViewController *mainView =  [[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil];
[mainView view];

id mock =  [OCMockObject partialMockForObject:mainView];

//testButtonPressed IBAction should be triggered
[[mock expect] testButtonPressed:[OCMArg any]];

//simulate button press 
[mainView.testButton sendActionsForControlEvents: UIControlEventTouchUpInside];

[mock verify];

Если IBAction не подключен, тест завершится с ошибкой "ожидаемый метод не был вызван".

person sash    schedule 09.09.2013
comment
Кажется, я получаю, что ожидаемый метод не был вызван, хотя IBOutlet подключен. У меня даже есть NSLog в viewDidLoad, который отображает правильную ссылку на UIButton. - person jowie; 08.12.2014

Вот что я использую в Swift. Я создал вспомогательную функцию, которую могу использовать во всех своих модульных тестах UIViewController:

func checkActionForOutlet(outlet: UIButton?, actionName: String, event: UIControlEvents, controller: UIViewController)->Bool{
    if let unwrappedButton = outlet {
        if let actions: [String] = unwrappedButton.actionsForTarget(controller, forControlEvent: event)! as [String] {
            return(actions.contains(actionName))
        }
    }
    return false
}

И затем я просто вызываю его из теста следующим образом:

func testScheduleActionIsConnected() {
    XCTAssertTrue(checkActionForOutlet(controller.btnScheduleOrder, actionName: "scheduleOrder", event: UIControlEvents.TouchUpInside, controller: controller ))
}

Я в основном удостоверяюсь, что кнопка btnScheduleOrder имеет IBAction, связанную с именем scheduleOrder для события TouchUpInside. Мне нужно передать контроллер, в котором находится кнопка, чтобы также проверить цель действия.

Вы также можете сделать его немного более сложным, добавив какое-то другое предложение else на случай, если unwrappedButton не существует, что означает, что выхода нет. Поскольку мне нравится разделять тесты розеток и действий, я не включил их сюда.

person Julio Bailon    schedule 26.04.2016
comment
Я добавил перевод этого для Swift 3 ниже - person Abbey Jackson; 04.01.2017
comment
Обновлено для Swift 4, спасибо Abbey и Julio. - person Rocket Garden; 28.02.2019

Ответ Хулио Бейлона выше, переведенный для Swift 3:

    func checkActionForButton(_ button: UIButton?, actionName: String, event: UIControlEvents = UIControlEvents.touchUpInside, target: UIViewController) -> Bool {

    if let unwrappedButton = button, let actions = unwrappedButton.actions(forTarget: target, forControlEvent: event) {

        var testAction = actionName
        if let trimmedActionName = actionName.components(separatedBy: ":").first {
            testAction = trimmedActionName
        }

        return (!actions.filter { $0.contains(testAction) }.isEmpty)
    }

    return false
}
person Abbey Jackson    schedule 04.01.2017

Таким образом, вероятно, можно создать экземпляр контроллера представления из раскадровки или пера, а затем выполнить касание UIButton. Однако я бы не стал этого делать, потому что вы тестируете стандартный API Apple. Скорее, я бы проверил, напрямую вызвав метод. Например, если у вас есть метод - (IBAction)foo:(id)sender в вашем контроллере представления, и вам нужно проверить логику в этом методе, я бы сделал что-то вроде этого:

MyViewController *viewController = [[MyViewController alloc] initWithNibName:@"NibName" bundle:[NSBundle mainBundle]];

UIButton *sampleButton = [[UIButton alloc] init];
[sampleButton setTitle:@"Default Storyboard Title" forState:UIControlStateNormal];

[viewController foo:sampleButton];

// Run asserts now on the logic in foo:
person Christian Di Lorenzo    schedule 09.09.2013
comment
Это проверит логику, но не проверит, действительно ли UIButton подключен в Interface Builder. - person jowie; 08.12.2014

Версия @AbbeyJackson Swift 3 обновлена ​​до Swift 4.2, спасибо @JulioBailon за исходную версию.

 func checkActionForButton(_ button: UIButton?, actionName: String, event: UIControl.Event = UIControl.Event.touchUpInside, target: UIViewController) -> Bool {
        if let unwrappedButton = button, let actions = unwrappedButton.actions(forTarget: target, forControlEvent: event) {
            var testAction = actionName
            if let trimmedActionName = actionName.components(separatedBy: ":").first {
                testAction = trimmedActionName
            }
            return (!actions.filter { $0.contains(testAction) }.isEmpty)
        }
        return false
    }
person Rocket Garden    schedule 28.02.2019

Здесь уже много хороших ответов. Что лучше всего подходит для вас, зависит от вашего плана тестирования.

Поскольку этот вопрос превратился в обзор методов тестирования, вот еще один: если вы хотите проверить результаты манипулирования пользовательским интерфейсом вашего приложения, загляните в Инструмент автоматизации пользовательского интерфейса в инструментах.

person rickster    schedule 11.09.2013