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:
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.You can't stub the
initWithTitle:delegate:cancelButtonTitle:destructiveButtonTitle:otherButtonTitles:
method and check its arguments because of the nil-terminated arguments.- 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 anEXC_BAD_ACCESS
. - 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.