Channels ▼
RSS

Testing

Automating iOS Application Testing: Under the Hood, Capturing and Recording Events


View Listening

In iOS some user interactions are reported via the Cocoa Touch target-action mechanism. For example, UISliders report value changes as target-actions.

To monitor a target-action, FoneMonkey sometimes needs to add its own listeners to a UI component upon its creation. FoneMonkey injects listener logic into the constructors of various classes by swizzling their –init methods.

For example, by swizzling the -initWithCoder: method of UIControl, FoneMonkey sets up a framework for customizing target-action recording.

@implementation UIControl (FoneMonkey)

+ (void)load {
    if (self == [UIControl class]) {
// Hijack UIControl's initialializers so we can add FoneMonkey stuff // whenever a UIControl is created

        Method originalMethod = 
            class_getInstanceMethod(self, @selector(initWithCoder:));
        Method replacedMethod = 
            class_getInstanceMethod(self, @selector(fmInitWithCoder:));
        method_exchangeImplementations(originalMethod, replacedMethod);        
    }
}

- (void) subscribeToMonkeyEvents {
    if (self.monkeyEventsToHandle != 0) {
[self addTarget:self action:@selector(handleMonkeyEventFromSender:forEvent:) forControlEvents:self.monkeyEventsToHandle];
    }
}

- (id)fmInitWithCoder:(NSCoder *)decoder {
    // Calls original initWithCoder (that we swapped in load method)
    if (self = [self fmInitWithCoder:decoder]) {    
        [self subscribeToMonkeyEvents];
    }
    return self;    
}

- (UIControlEvents) monkeyEventsToHandle {
    // Default ignores all events
    return 0;
}
- (void) handleMonkeyEventFromSender:(id)sender forEvent:(UIEvent*)event {
    // Default is a no-op
}

@end

In the code above, we essentially inject a call to -subscribeToMonkeyEvents: into the constructor of all UIControls. This method in turn adds a target-action listener for all control events, filtered according to the mask returned by -monkeyEventsToHandle:. By overriding –monkeyEventsToHandle: in a category, you can customize recording for a UIControl subclass. For example, the code below causes the recording of a "Slide" command whenever the value of a UISlider changes.

@implementation UISlider (FMReady)

- (UIControlEvents)monkeyEventsToHandle {
        return UIControlEventValueChanged;
}


- (void) handleMonkeyEventFromSender:(id)sender forEvent:(UIEvent*)event {
        UITouch* touch = [[event allTouches] anyObject];
        [FoneMonkey recordFrom:self command:FMCommandSlide 
args:[NSArray arrayWithObject:[[NSString alloc]   initWithFormat:@"%.2f",self.value]]]; 
}

Delegate Interception

In the techniques discussed so far, we've been able to intercept method calls by creating a category that swizzles the +load or -init methods of its associated class. Unfortunately, delegates are assigned to views at runtime, and can usually be of any class implementing some informal protocol. Since we have no real way of knowing which classes implement an informal protocol, we swizzle the -setDelegate: method of a user interface class so that we can know whenever a delegate instance is assigned, and we can then set up method interception to any delegate methods requiring monitoring.

One interesting complication of delegate method interception is that delegates are instance-specific. While one instance of a particular user interface class might be associated with the delegate of one class, a different instance of the same user interface class can be associated with an entirely different delegate class. For example, different instances of UITableView are typically assigned delegate instances from different classes implementing the UITableViewDelegate protocol.

Because we might associate instances of several different delegate classes with instances of a single user interface class, we can't just swap method implementations. We instead give a new name to the method to be intercepted, and then point the original selector to our replacement implementation. (Figure 5.)

Figure 5: When setDelegate is called on an instance of SomeClass, delegate method implementations are replaced with an intercepting method implementation. The original method implementation is given a new selector name that the interceptor can later use to invoke it.

For example, the following code shows an example of intercepting delegate calls for a UIPickerView.

@implementation UIPickerView (FMReady)    

+ (void)load {
    if (self == [UIPickerView class]) {
        
 Method originalMethod =       class_getInstanceMethod(self, @selector(setDelegate:));
        Method replacedMethod =             class_getInstanceMethod(self, @selector(fmSetDelegate:));
        method_exchangeImplementations(originalMethod, replacedMethod);        
    }
}

- (void) fmSetDelegate:(id <UIPickerViewDelegate>) del {
    Method originalMethod = class_getInstanceMethod([del class],                             @selector(pickerView:didSelectRow:inComponent:));
    if (originalMethod) {
          IMP origImp = method_getImplementation(originalMethod);
          Method replacedMethod = class_getInstanceMethod([self class],                            @selector(fmPickerView:didSelectRow:inComponent:));
          IMP replImp = method_getImplementation(replacedMethod);
        
          if (origImp != replImp) {
            method_setImplementation(originalMethod, replImp);
            class_addMethod([del class],                @selector(origPickerView:didSelectRow:inComponent:),             origImp,"v@:@ii");
          }
    }
    [self fmSetDelegate:del];
}

- (void)fmPickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    [ FoneMonkey recordEvent:[[FMCommandEvent alloc]        init:FMCommandSelect className:@"UIPickerView"       monkeyID:[pickerView monkeyID]       args:[NSArray arrayWithObjects:       [NSString stringWithFormat: @"%d", row],       [NSString stringWithFormat:@"%d", component], nil]]];
      [self origPickerView:pickerView didSelectRow:row inComponent:component];
}    

The category above swizzles the –setDelegate: method so that whenever a delegate is set, its -pickerView:didSelectRow:inComponent: method is renamed to -origPickerView:didSelectRow:inComponent: via the call to class_addMethod. (The string argument "v@:@ii" is an encoded list containing the arguments and returns types of the method as required by the class_addMethod function). The -pickerView:didSelectRow:inComponent: selector is then set to point to the category's -fmPickerView:didSelectRow:inComponent: implementation instead.

When this replacement method is called in response to a user interacting with a UIPickerView, it records the picker's new selection and then invokes the original delegate method by calling [self origPickerView:didSelectRow:inComponent:]. Because the method is defined in UIPickerView(FMReady) , it might at first seem that self is referring to a UIPickerView instance, but since this is a replacement method implementation being invoked on a delegate class instance at run-time, self refers to a UIPickerViewDelegate instance, and so the call will invoke the original method on the class corresponding to the delegate instance. In this way, the interceptor is able to intercept and then forward calls to any number of delegate classes.

Perhaps surprisingly, the previous example replaces a method implementation on one class (UIPickerView) with a method implementation from a completely unrelated class (any class implementing the UIPickerViewDelegate protocol). This works because the moved method's references to self are via selectors that are valid at compile-time in a UIPickerView, but are also valid at run-time by any class implementing the UIPickerViewDelegateProtocol.

Command Playback

FoneMonkey plays back a command by finding a user interface component that matches the supplied class and monkeyID, and by then calling -playbackMonkeyEvent: on the found component. A default implementation of this method is provided by the UIView (FMReady) category.

The default -playbackMonkeyEvent: implementation plays back Touch commands by programmatically creating UITouch events and using these to call the -touchesBegan: and -touchesEnded: UIResponder methods of the component for which touching is to be simulated.

Customizing Command Playback

You can customize command playback for a user interface class by overriding the -playbackMonkeyEvent: method in an associated category. For example, the code below customizes playback logic for a UIPickerView.

@implementation UIPickerView (FMReady)    

- (void) playbackMonkeyEvent:(FMCommandEvent*)event {
    if ([event.command isEqual:FMCommandSelect]) {
        if ([event.args count] == 0) {
            event.lastResult =               @"Requires 1 or 2 arguments (row #, component #)";
            return;
        } 
        NSInteger row = [[event.args objectAtIndex:0] intValue];
        NSInteger component = 0;
        if ([event.args count] == 2) {
            component = [[event.args objectAtIndex:1] intValue];
        }
        
        [self selectRow:row inComponent:component animated:NO];
        [self.delegate origPickerView:self         didSelectRow:row inComponent:component];        
    }
}

In the preceding code, the FMCommandEvent's arguments specify the picker's selected values. If an incorrect number of arguments are specified, an error message is set on the FMCommandEvent. Otherwise, the values are passed to the picker's -selectRow:inComponent:animated: method in order to update the UIPickerView to the values specified on the command. After updating the picker, the necessary delegate method is invoked explicitly by calling -origPickerView:didSelectRow:inComponent:. As discussed earlier, UIPIckerView (FMReady) renames -pickerView:didSelectRow:inComponent: so that invoking -origPickerView:didSelectRow:inCononent: actually invokes the original delegate method implementation.

Conclusion

In this article, I explored how FoneMonkey can be used to automate functional testing of native iOS applications. In this second and final part, we've reviewed the various mechanisms FoneMonkey uses for recording and playing back user interface interactions, and how these mechanisms allow FoneMonkey to monitor events, component actions, and delegate calls. Its ability to intercept both low-level events and semantically higher-level method invocations allows FoneMonkey to record single commands in place of lengthy event streams.

We've seen how Objective-C method swizzling can be used to intercept method invocations, and how FoneMonkey uses this technique to monitor an application's event stream by intercepting calls to UIApplication's -sendEvent: method. Swizzling is also used to intercept

–init calls and inject logic to add event and notification listeners to some user interface components. For intercepting delegate method calls, FoneMonkey uses a modified approach that renames and replaces methods rather than swapping them.

We've also examined how FoneMonkey uses Objective-C categories to add recording and playback logic to iOS user interface classes, and how you can use categories to customize FoneMonkey recording and playback behavior.

I hope that this under-the-cover look might enable the FoneMonkey user community to participate in FoneMonkey's ongoing evolution. (Although beyond the scope of this article, we should point out that the various method interception techniques reviewed here can be used to implement aspect-oriented programming frameworks in Objective-C. Have fun!).

FoneMonkey downloads, source, and documentation are available for further reference.


Stu Stern is a Founder and the CEO of Gorilla Logic Inc., the developer of FoneMonkey and other tools.

Related Article: Project of the Month: Automating iOS Application GUI Testing With FoneMonkey


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video