3

I've got an app where I push a UIAlertController with several custom UIAlertActions. Each UIAlertAction performs unique tasks in the handler block of actionWithTitle:style:handler:.

I have several methods that I need to verify are executed within these blocks.

How can I execute the handler block so that I can verify that these methods are executed?

Drew H
  • 1,226
  • 1
  • 12
  • 13

3 Answers3

4

After some playing around I finally figure it out. Turns out that the handler block can be cast as a function pointer and the function pointer can be executed.

Like so

UIAlertAction *action = myAlertController.actions[0];
void (^someBlock)(id obj) = [action valueForKey:@"handler"];
someBlock(action);

Here's an example of how it would be used.

-(void)test_verifyThatIfUserSelectsTheFirstActionOfMyAlertControllerSomeMethodIsCalled {

    //Setup expectations
    [[_partialMockViewController expect] someMethod];

    //When the UIAlertController is presented automatically simulate a "tap" of the first button
    [[_partialMockViewController stub] presentViewController:[OCMArg checkWithBlock:^BOOL(id obj) {

        XCTAssert([obj isKindOfClass:[UIAlertController class]]);

        UIAlertController *alert = (UIAlertController*)obj;

        //Get the first button
        UIAlertAction *action = alert.actions[0];

        //Cast the pointer of the handle block into a form that we can execute
        void (^someBlock)(id obj) = [action valueForKey:@"handler"];

        //Execute the code of the join button
        someBlock(action);
    }]
                                         animated:YES
                                       completion:nil];

   //Execute the method that displays the UIAlertController
   [_viewControllerUnderTest methodThatDisplaysAlertController];

   //Verify that |someMethod| was executed
   [_partialMockViewController verify];
}
Drew H
  • 1,226
  • 1
  • 12
  • 13
  • 1
    Any idea what the correct cast is in Swift? Simply casting to the expected function type gives an `EXC_BAD_INSTRUCTION` at runtime. – Robert Atkins Sep 15 '16 at 11:01
  • I wish I could upvote this answer twice. Big help for mocking around alert action handlers. I understand Apple could change how this works and my tests will break, but I'm willing to live with it for now. – Bill Burgess Feb 19 '20 at 14:53
2

With a bit of clever casting, I've found a way to do this in Swift (2.2):

extension UIAlertController {

    typealias AlertHandler = @convention(block) (UIAlertAction) -> Void

    func tapButtonAtIndex(index: Int) {
        let block = actions[index].valueForKey("handler")
        let handler = unsafeBitCast(block, AlertHandler.self)

        handler(actions[index])
    }

}

This allows you to call alert.tapButtonAtIndex(1) in your test and have the correct handler executed.

(I would only use this in my test target, btw)

Robert Atkins
  • 23,528
  • 15
  • 68
  • 97
  • This fails in Swift 3.0.1 with `fatal error: can't unsafeBitCast between types of different sizes`, does anyone know how to fix this? – Robert Atkins Nov 15 '16 at 14:21
0

The following code will work on Swift 5.5 I would suggest to create a private DSL in the test target

private extension UIAlertController {

typealias AlertHandler = @convention(block) (UIAlertAction) -> Void

func tapButtonAtIndex(index: Int) {
    let alertAction = actions[index]
    let block = alertAction.value(forKey: "handler")
    let handler = unsafeBitCast(block, to: AlertHandler.self)
    handler(alertAction)
  }
}
Sishu
  • 1,510
  • 1
  • 21
  • 48