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.
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_idsNew 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 signatureAction 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_eventsSummary
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.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.