Is it possible to test IBAction?
Asked Answered
H

7

7

It is kinda easy to unit test IBOutlets, but how about IBActions? I was trying to find a way how to do it, but without any luck. Is there any way to unit test connection between IBAction in a View Controller and a button in the nib file?

Hair answered 9/9, 2013 at 13:38 Comment(2)
What you mean by "testing" ? Do you want to make unit test for those ? If so than just encapsulate your action into functions / classes and test those classes with unit tests. If you have sometehing else on your mind -please explain it a bit more.Flexion
Yes, I want to write unit test.Hair
H
5

I did it using OCMock, like this:

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];

If IBAction is not connected, the test will fail with error "expected method was not invoked".

Hair answered 9/9, 2013 at 15:46 Comment(1)
I seem to be getting "expected method was not invoked", even though the IBOutlet is wired up. I even have an NSLog in the viewDidLoad which displays the correct reference to the UIButton.Adjourn
B
10

For full unit testing, each outlet/action needs three tests:

  1. Is the outlet hooked up to a view?
  2. Is the outlet connected to the action we want?
  3. Invoke the action directly, as if it had been triggered by the outlet.

I do this all the time to TDD my view controllers. You can see an example in this screencast.

It sounds like you're asking specifically about the second step. Here's an example of a unit test verifying that a touch up inside myButton will invoke the action doSomething: Here's how I express it using OCHamcrest. (sut is a test fixture for the system under test.)

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

Alternatively, here's a version without Hamcrest:

- (void)testMyButtonAction {
    NSArray *actions = [sut.myButton actionsForTarget:sut
                              forControlEvent:UIControlEventTouchUpInside];
    XCTAssertTrue([actions containsObject:@"doSomething:"]);
}
Breechloader answered 10/9, 2013 at 22:18 Comment(4)
I am using unit testing so how to check that this button is click using like- XCTAssertTrue etc.Edmondson
I am using segue on button so how can i test ?Edmondson
In order to do it this way, you have to create an IBOutlet specifically for testing purposes. Also you have to refer to the handler in a string, which means it will break if refactored.Adjourn
@jowie: AppCode automatically refactors things like that. But even without AppCode, I'm willing to pay the small price because of the ease & speed with which I can get feedback.Breechloader
H
5

I did it using OCMock, like this:

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];

If IBAction is not connected, the test will fail with error "expected method was not invoked".

Hair answered 9/9, 2013 at 15:46 Comment(1)
I seem to be getting "expected method was not invoked", even though the IBOutlet is wired up. I even have an NSLog in the viewDidLoad which displays the correct reference to the UIButton.Adjourn
J
5

Here is what I use in Swift. I created a helper function that I can use in all my UIViewController unit tests:

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
}

And then I just invoke it from the test like this:

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

I am basically making sure that the button btnScheduleOrder has an IBAction associated with the name scheduleOrder for the event TouchUpInside. I need to pass the controller where the button is contained as a way to verify the target for the action as well.

You can also make it a little more sophisticated by adding some other else clause in case the unwrappedButton does not exist which means the outlet is not there. As I like to separate outlets and actions tests I don't have it included here

Jennettejenni answered 26/4, 2016 at 2:15 Comment(2)
I added a translation of this for Swift 3 belowPagan
Updated for Swift 4, thanks to both Abbey and JulioMannino
P
2

Julio Bailon's answer above translated for 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
}
Pagan answered 4/1, 2017 at 19:8 Comment(0)
A
1

So, it is probably possible to instantiate a view controller from a storyboard or nib and then do a touch down on a UIButton. However, I wouldn't do this because you are then testing an Apple stock API. Rather, I would test by directly calling the method. For example if you have a method - (IBAction)foo:(id)sender in your view controller and you need to test the logic in that method, I would do something like this:

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:
Acred answered 9/9, 2013 at 13:51 Comment(1)
That will check the logic, but that won't check that the UIButton is actually wired up in Interface Builder.Adjourn
M
0

@AbbeyJackson Swift 3 version updated to Swift 4.2, thanks to @JulioBailon for the original version.

 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
    }
Mannino answered 28/2, 2019 at 15:5 Comment(0)
K
-1

Lots of good answers here already. Which works best for you depends on your testing plan.

Since this question has turned into a survey on testing methods, here's one more: if you want to test the results of manipulating your app's UI, look into the UI Automation tool in Instruments.

Korikorie answered 11/9, 2013 at 0:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.