Channels ▼
RSS

Tools

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


In an earlier article, I looked at how FoneMonkey, the free and open source testing tool from Gorilla Logic, can be used to automate functional testing of native iOS apps. In this article, I take a look under the hood at the several interesting and unusual techniques FoneMonkey uses to achieve its recording and playback magic. This should provide a deeper understanding of user interface handling in iOS, as well as some advanced Objective-C coding techniques for doing things like intercepting method calls.

How FoneMonkey Recording Works

As every iOS developer knows, user interactions are delivered to application code via any of several different mechanisms including UITouch events, UIControl actions, UINotifications, and various delegate protocols. FoneMonkey does recording and playback via techniques that work in conjunction with each of these mechanisms.

It may at first seem like you could implement a tool like FoneMonkey by simply recording and replaying every UI event received by an application. The resulting blizzard of events, however, are too numerous and in many cases too low-level to produce readable or maintainable scripts. Events need to be filtered, aggregated, and translated to the semantics of UI components in order to provide understandable automation scripts.

For example, a swipe on a UITableView consists of the initial touch, a series of events reporting the dragging of your finger, and a final event reporting the end of the gesture. The result is that the table scrolls. Such a sequence could be recorded verbatim as the complete event stream, or it could be aggregated into a single "Swipe" command with a starting and ending coordinate, or — as FoneMonkey does it — as a semantically richer "Scroll" command with arguments specifying to which row the table should be scrolled.

From a readability and maintainability perspective, and to enable users to create an automation command from scratch without recording, most users would prefer an approach that allows you to say "Scroll the table to row 4," rather than "Swipe the table from this pixel coordinate to that coordinate." As another example, consider moving a slider. Again, you could script this as a series of events corresponding to the finger drag or a swipe vector, or specify that the Slider should be moved to a new value (not coordinate), which is how FoneMonkey does it.

Sometimes simple touch interactions such as pushing a button can be recorded understandably as a "Touch" command rather than as something semantically richer. Pushing a button can be recorded as something like "Touch Button" with virtually no loss in script readability.

Because some interactions an be recorded as lower-level commands such as "Touch," and because others are better represented as higher-level commands like "Scroll" or "Slide," FoneMonkey injects monitoring into various points along the iOS UI-processing chain to capture interactions at different "semantic levels."

Figure 1: Monitoring and capturing events at different semantic levels.

Because recording and playback logic is often component-type-specific, FoneMonkey implements recording and playback logic as category extensions (called "FMReady") to various UIKit classes. Fortunately, most recording and playback logic is inherited by subclasses without requiring further specialization. As we'll see below, it is straightforward to extend FoneMonkey recording and playback by implementing your own category extensions.

Segregating extensions in Objective-C categories makes it easy to set up your builds so that testing-specific extensions are compiled and linked into your application ony when required for testing, and be omitted from non-test builds.

Event Filtering

FoneMonkey implements event filtering by intercepting calls to the -sendEvent: method of the application's UIApplication class. All touch and motion (shake) events are first delivered to this method. FoneMonkey accomplishes method interception through a technique known in Objective-C as "method swizzling," which allows the method implementation of one class to be swapped with that of another.

Method Interception

FoneMonkey intercepts -sendEvent: as well as various other methods of iOS UI classes with Objective-C categories that swap ("swizzle") the original method of a class with a replacement method from an associated category. (Figure 2)

Figure 2: FoneMonkey uses Objective-C categories and method swizzling to intercept method calls.

For example, in the code below, a UIAppliction category swaps UIApplication's sendEvent: implementation with its own method called fmSendEvent:

@implementation UIApplication (FMReady)

+ (void)load {
    if (self == [UIApplication class]) {
        
        Method originalMethod = class_getInstanceMethod(self, @selector(sendEvent:));
        Method replacedMethod = class_getInstanceMethod(self, @selector(fmSendEvent:));
        method_exchangeImplementations(originalMethod, replacedMethod);
    }
}


// This method will be called in place of sendEvent:
- (void)fmSendEvent:(UIEvent *)event {

  // Send the event to FoneMonkey for possible recording
    [[FoneMonkey sharedMonkey] handleEvent:event];
    
    // Call the original (now renamed) sendEvent:
    [self fmSendEvent:event];

}
@end

In Objective-C, a class's static +load method is called when the class is first loaded, providing for additional static initialization of a class. In the above code, FoneMonkey uses the +load method to swap the implementation of sendEvent: with fmSendEvent:. The method_exchangeImplementations call swaps the method implementations such that calls to sendEvent: at run-time result in calls to the -fmSendEvent: method defined in the category.

-fmSendEvent: forwards the received event to the handleEvent: method of [FoneMonkey sharedMonkey], a singleton instance of the FoneMonkey class. (This singleton instance provides many of FoneMonkey's core API's for event handling, recording, and playback). The next line calls -fmSendEvent: which appears to be a recursive call, but because we've swizzled the methods, the selector now refers to the original (now renamed) -sendEvent: method.

Figure 3: FoneMonkey monitors events by intercepting calls to sendEvent:.

Customizing Event Recording

By default, FoneMonkey records a Touch command whenever a UITouch with UITouchPhaseEnded ("touch up") is received. This default functionality is implemented in FoneMonkey's UIView (FMReady) category as well as several subclasses that specialize the handling for various kinds of UI components. You can filter which touch events are recorded for some component class by overriding the -shouldRecordMonkeyTouch: method in an associated class category. For example, the class below changes recording for MYCustomView so that it records only "touch down" events.

@implementation MYCustomView (FMReady)

- (BOOL) shouldRecordMonkeyTouch:(UITouch*)touch {
    return (touch.phase == UITouchPhaseEnded);
}

@end

If -recordMonkeyTouch: returns YES, then FoneMonkey calls the category's -handleMonkeyTouchEvent:withEvent: method.

For example, the code below records a custom "Start" command at the beginning of a touch, and an "End" command when it's finished.

@implementation MYCustomView (FMReady)


- (BOOL) shouldRecordMonkeyTouch:(UITouch*)touch {
    return (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseEnded);
}

- (void) handleMonkeyTouchEvent:(NSSet*)touches withEvent:(UIEvent*)event {
UITouch* touch = [touches anyObject];
NSString* cmd = touch.phase == UITouchPhaseBegan : "Start" : "End";
[FoneMonkey recordFrom:self command:cmd args:nil];
@end

This action is illustrated in Figure 4.

Figure 4: FoneMonkey recording for a user interface component is implemented in an associated category.

Recording Finger Dragging

Some components, such as painting canvases, respond to finger-dragging motions. Finger dragging generates a touch event for a set of (x,y) coordinates comprising the path of the dragging gesture. FoneMonkey coalesces this set of events into a single "Move" command with an argument list containing the x-y pairs.

To enable drag recording for some class, override shouldRecordMonkeyTouch: similar to described above.

@implementation MYCustomView (FMReady)


- (BOOL) shouldRecordMonkeyTouch:(UITouch*)touch {
    return (touch.phase == UITouchPhaseMoved);
}

The above category causes finger dragging on the custom view to be recorded. Each drag operation will be recorded with a "Move" command similar to the one below, which corresponds to a diagonal path from (0,0) to (4,4).

Move MYCustomView theView 0,0,1,1,2,2,3,3,4,4


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