Defining Android UI Pages and Filtering Action Events for Model‑Based Testing
This article explains how to represent Android UI view trees as lists, generate unique page signatures, and filter actionable events such as scrolls and touches to improve model‑based testing coverage and traceability, providing Python code examples for each step.
In Android UI model‑based testing, defining UI pages and selecting relevant action events are essential for achieving higher coverage and traceability compared to random monkey testing. The UI hierarchy, called a view tree, consists of nodes (sub‑views) that contain attributes like resource_id, clickable, scrollable, and bounds. Converting this tree into a flat list while preserving parent‑child relationships enables systematic processing.
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_idsTo distinguish pages that may refresh indefinitely (e.g., feed pages), each page is represented by a textual signature composed of selected sub‑view attributes ( class, clickable, checked, scrollable, long‑clickable, text) and then 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 proceeds by first discarding system navigation bar views (identified by known resource_id values) and then selecting sub‑views whose scrollable or clickable flags are true. Additional criteria such as enabled or focusable can be added as needed.
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_eventsFurther filtering removes events whose bounding coordinates lie outside the device screen or whose height/width is less than five pixels, ensuring only meaningful interactions are kept.
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_eventsBy defining pages through hashed signatures and filtering actionable events, different UI states can be grouped, redundant interactions eliminated, and a directed graph of UI states can be constructed. This graph enables the application of traversal algorithms such as depth‑first search, heuristic search, or reinforcement learning to achieve thorough automated testing of Android applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
360 Quality & Efficiency
360 Quality & Efficiency focuses on seamlessly integrating quality and efficiency in R&D, sharing 360’s internal best practices with industry peers to foster collaboration among Chinese enterprises and drive greater efficiency value.
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.
