Mobile Development 8 min read

Model-Based Android UI Testing: View Tree Definition and Action Event Filtering

This article explains how to define Android UI pages as view trees, convert them to list structures, generate unique page signatures, and filter actionable events such as scrollable and clickable views using Python, enabling more effective model‑based testing of mobile applications.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Model-Based Android UI Testing: View Tree Definition and Action Event Filtering

In Android UI traversal testing, model‑based testing improves coverage and traceability compared to random monkey testing. Defining UI pages and selecting actionable events are essential steps, and this guide presents concrete methods for both.

Technical Implementation

The UI hierarchy is obtained via UI Automator and represented as a view tree . To simplify processing, the tree is flattened into a list while preserving each sub‑view's index and parent relationship.

def get_view_list(view_tree):
    view_tree['parent'] = -1
    view_list = []
    view_tree_to_list(0, view_tree, view_list)
    self.last_acc_event['view_list'] = view_list
    return view_list


def view_tree_to_list(index, view_tree, view_list):
    tree_id = len(view_list)
    view_tree['temp_id'] = tree_id
    bounds = [[-1, -1], [-1, -1]]
    bounds[0][0] = view_tree['bounds'][0]
    bounds[0][1] = view_tree['bounds'][1]
    bounds[1][0] = view_tree['bounds'][2]
    bounds[1][1] = view_tree['bounds'][3]
    width = bounds[1][0] - bounds[0][0]
    height = bounds[1][1] - bounds[0][1]
    view_tree['size'] = "%d*%d" % (width, height)
    view_tree['index'] = index
    view_tree['bounds'] = bounds
    view_list.append(view_tree)
    children_ids = []
    for item in range(len(view_tree['children'])):
        child_tree = view_tree['children'][item]
        child_tree['parent'] = tree_id
        view_tree_to_list(item, child_tree, view_list)
        children_ids.append(child_tree['temp_id'])
    view_tree['children'] = children_ids

New Page Definition

To avoid infinite loops on endlessly scrollable pages (e.g., feed pages), each page is represented by a hash of its sub‑view attributes ( class , clickable , checked , scrollable , long‑clickable , text ). The concatenated text is hashed with MD5 to obtain a unique identifier.

def get_state_str(view_list):
    state_str_raw = get_state_str_raw(view_list)
    return md5(state_str_raw)


def get_state_str_raw(view_list):
    view_signatures = set()
    for view in view_list:
        view_signature = get_view_signature(view)
        if view_signature:
            view_signatures.add(view_signature)
    return "%s{%s}" % (self.foreground_activity, ",".join(sorted(view_signatures)))


def get_view_signature(view_dict):
    view_text = view_dict['text']
    if view_text is None or len(view_text) > 50:
        view_text = "None"
    signature = "[class]%s[text]%s[%s,%s,%s,%s]" % (
        view_dict['class'],
        view_text,
        view_dict['clickable'],
        view_dict['checked'],
        view_dict['scrollable'],
        view_dict['long-clickable']
    )
    return signature

Action Event Filtering

All sub‑views are examined; navigation bar views (identified by specific resource_id ) are excluded. Remaining enabled views are classified as either scrollable or clickable, and corresponding ScrollEvent or TouchEvent objects are generated. Additional filters can remove views with out‑of‑bounds coordinates or those occupying less than five pixels.

def get_possible_input(view_list):
    possible_events = []
    enabled_view_ids = []
    touch_exclude_view_ids = set()
    for view_dict in view_list:
        if view_dict['enabled'] and view_dict['resource_id'] not in ['android:id/navigationBarBackground', 'android:id/statusBarBackground']:
            enabled_view_ids.append(view_dict['temp_id'])
    for view_id in enabled_view_ids:
        if view_list[view_id]['scrollable']:
            possible_events.append(ScrollEvent(view=views_list[view_id], direction="UP"))
            possible_events.append(ScrollEvent(view=views_list[view_id], direction="DOWN"))
            possible_events.append(ScrollEvent(view=views_list[view_id], direction="LEFT"))
            possible_events.append(ScrollEvent(view=views_list[view_id], direction="RIGHT"))
        elif view_list[view_id]['clickable']:
            possible_events.append(TouchEvent(view=views_list[view_id]))
            touch_exclude_view_ids.add(view_id)
    return possible_events


def filter_possible_input(possible_events, origin_dim=[1080, 1920]):
    filter_events = []
    for event in possible_events:
        bounds = event.view["bounds"]
        bounds = [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]]
        x_min = max(0, bounds[0])
        y_min = max(0, bounds[1])
        x_max = min(origin_dim[0], bounds[2])
        y_max = min(origin_dim[1], bounds[3])
        if x_min >= x_max or y_min >= y_max:
            continue
        event.view["bounds"] = [[x_min, y_min], [x_max, y_max]]
        if (y_max - y_min) < 5:
            pass
        else:
            filter_events.append(event)
    return filter_events

Summary

By defining pages through view‑tree signatures and filtering actionable events, similar pages can be grouped and irrelevant interactions discarded. This foundation enables building graph models of the UI, where traversal algorithms—such as depth‑first search, heuristic search, deep learning, or reinforcement learning—can be applied to achieve thorough automated testing of Android applications.

mobile developmentpythonAndroidView TreeUI testingModel-Based TestingEvent Filtering
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.