3

I'm trying to write unit tests for my view controller. It is the first view controller in my app and has a 'Account' button on the top left hand corner. Pressing this will present an action sheet, which, for now, has two buttons:

  • Logout
  • Change Passcode

I want to write tests for this functionality:

  • Pressing the 'Account' button should present Action Sheet.
  • The Action Sheet should have two buttons: 'Logout' & 'Change Passcode'.
  • Pressing the 'logout' button should log the user out.
  • Pressing the 'change passcode' button should present the passcode view controller in change passcode mode.

The problem is, if I trigger the Account button in my test, it will try to present an action sheet, which fails because the view of the controller under test is not part of a window, and that means I can't write any of the other tests either.

There are proposed solutions on testing alert views and action sheets, but they require creating a PONSO with the same interface as UIActionSheet, and doing something like this in my view controller:

// in the .h file
@property (nonatomic, strong) Class actionSheetClass;

// in the .m file
// after button is pressed...
self.actionSheetClass actionSheet = [[self.actionSheetClass alloc] init...];

This is a very unnatural way of writing code - one of those times when you twist your code out of shape just to make it testable. I get better test coverage at the expense of readability. I'd rather have my cake, eat it, and then bake some more cake and eat that too.

Does anyone know how I can test my UIActionSheet-based behaviour without resorting to such shenanigans?

Update

After Michał Ciuba's comment, I started exploring how I can use OCMock to test all the things I want to test. The problem with the approach in that thread is that the number of buttons cannot be tested, neither can their actions. The culprit is the nil-terminated argument list. Even with all the acrobatics that the Objective-C runtime makes possible, it's actually impossible to test the buttons and their actions.

This is why:

  1. You can't show an action sheet because your tests don't have a view that is being displayed. So if the view controller uses some code like -(void) showActionSheet { UIActionSheet* actionSheet = [[UIActionSheet alloc] initWith...]; [actionSheet showInView:self.view]; } That is to say, if it doesn't hold a reference to its action sheet, you also won't be able to get a reference to the action sheet in your tests without some mocking. No reference, no checking buttons.

  2. You can't stub the initWithTitle:delegate:cancelButtonTitle:destructiveButtonTitle:otherButtonTitles: method and check its arguments because of the nil-terminated arguments.

  3. I tried to use Peter Steinberger's Aspects library to add an "after hook" to the that method, but the nil-terminated arguments again cause problems here because Aspects uses NSInvocation to pass the message onto the original method, which means attempting to access anything past the first item in a variable argument list will cause an EXC_BAD_ACCESS.
  4. Swizzling the init method? That might be an option but I haven't tried yet and won't have time to for a while.

Is that really worth the effort? Certainly not, but I think it should be testable and that it shouldn't take this much effort. It's definitely been educational.

Shinigami
  • 2,123
  • 23
  • 40
  • Maybe this answer will be helpful: http://stackoverflow.com/a/21173592/2128900 – Michał Ciuba Aug 01 '14 at 16:38
  • How about not initializing the buttons in `initWithTitle:delegate:cancelButtonTitle:destructiveButtonTitle:otherButtonTitles:`, but instead adding them one-by-one using `addButtonWithTitle:`? You should be able to capture this method and verify the arguments with OCMock. – Michał Ciuba Aug 03 '14 at 10:41
  • Yep, that'd work. In fact, that would be the sensible thing to do. But I was trying to be stubborn and not change ANYTHING in the way I created the action sheet ;) – Shinigami Aug 03 '14 at 21:43

0 Answers0