Възможно ли е да се тества IBAction?

Лесно е да тествате IBOutlets, но какво ще кажете за IBActions? Опитвах се да намеря начин как да го направя, но без успех. Има ли някакъв начин за единична тестова връзка между IBAction в View Controller и бутон в nib файла?


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
Използвам модулно тестване, така че как да проверя дали този бутон е щракнат с помощта на like- XCTAssertTrue и т.н. - person Vivek Yadav; 07.07.2014
comment
Използвам бутон Segue on, така че как мога да тествам? - 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

Отговорът на Julio Bailon по-горе е преведен за 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