目录
自动化测试的意义
在日常的软件测试过程中,往往存在大量的重复测试工作。如对于一款软件是在不断地更新迭代的,每推出一个新版本,除了对新功能进行测试外,对于以前的旧版本中的相关内容也是需要测试,这就需要测试人员不断地重复测试操作,耗时耗力。而使用自动化测试工具可以在短时间内快速执行大量重复的测试用例,并且能够始终按照预设的逻辑和步骤进行操作, 从而极大地提高了测试效率。
pywinauto
pywinauto属于GUI自动化测试框架的一个组成部分,GUI自动化适用于需要高效、准确、重复测试GUI应用程序的各种场景,有助于提升软件质量、 用户满意度和开发效率。但需要注意GUI自动化适用于界面不会频繁发生改变,交互不过于复杂的应用程序的场景
Pywinauto是一款基于Python的跨平台GUI自动化库,常用于Windows界面应用程序,其核心在于通过模拟用户交互行为(如鼠标点击、键盘输入)实现对窗口、对话框及内部控件的精准定位与操作,适用于自动化测试、批量任务处理及日常办公流程优化等场景。该库通过两种底层技术适配不同框架开发的应用程序,常用为uia(支持现代WinForms、WPF、Qt5及浏览器等应用)
1.优势
跨平台:支持Windows 7及以上版本,兼容性良好
基于python:python简易且容易上手,适合快速开发和维护
动态查找机制:自动等待控件加载完成,无需显式等待
社区活跃:开源项目,持续更新,文档和示例丰富
丰富的控件支持:支持Windows原生控件(如按钮、表格) 以及第三方控件(如WPF,Qt)
2.劣势
1.仅支持Windows:无法用于Mac或Linux平台
2.对非标准控件支持有限:某些自定义控件可能需要额外处理
3.pywinauto支持传统 Windows 原生应用框架和部分跨平台框架(需在 Windows 运行),但基于 Web 的应用程序和基于自定义渲染引擎的应用程序是不能进行交互的,不支持pywinauto
3.环境及安装
环境:python环境(建议版本3.8及以上)+pycharm+UISpy/ViewWizard(界面程序元素定位工具)
#安装pywinauto,建议指定版本,不同版本之间会有差异
pip install pywinauto==0.6.9
#查看pywinauto是否安装成功---有pywinauto的话,说明安装成功
pip list
注:应用程序可以通过UISpy工具定位到元素,说明该元素的实现是支持传统 Windows 原生应用框架,而无法识别的元素 则属于“不能用 pywinauto 进行自动化测试的”
也可以进行离线安装,可自查进行安装
4.打开程序
start
函数作用:打开应用程序
函数原型:
start(self, cmd_line......)
参数解释:
cmd_line :这是启动应用程序的命令行字符串。它必须包含应用程序的路径和名称,还可以包含启动参数
from pywinauto.application import Application
#打开记事本程序--系统自带的程序
Application(backend="uia").start("notepad.exe")
#可以采用绝对路径的方式打开程序--任意一个程序
Application(backend="uia").start("C:\\Windows\\System32\\notepad.exe")
connect
函数作用:连接已经打开的应用程序
函数原型:
connect(self, **kwargs)
参数解释:
process :目标的进程ID handle:窗口句柄
#第一种方式,通过start的返回值中获取process来进行connect
app=Application("uia").start("E:\\tools\\bit\\Sublime Text\sublime_text.exe")
Application(backend="uia").connect(process=app.process)
#第二种方式,先提前打开程序(没有start这一步),直接确定程序的processId
Application(backend="uia").conenct(process=30940)
#以上方式二选一即可

5.定位窗口
window
函数原型:
app.window(title='',...)
参数解释(参数可以自由组合使用)
title : 文本为指定值的元素
title_re : 文本匹配指定正则表达式的元素
best_match : 标题与指定值相似的元素
class_name : 窗口类为指定值的元素
class_name_re : 类名匹配指定正则表达式的元素

这里的Name为title定位窗口的参数
#title进行窗口定位
win=app.window(title="untitled • - Sublime Text (UNREGISTERED)")
但此时Sublime Text是没有内容的,如果Sublime Text中有内容的话

会发现Name也会发生改变,因此如果第一次窗口定位后,输入文本内容的话,再次使用原title定位的话就会失效,出现错误,可以采用title_re正则表达式的方式来进行匹配
对于输入文本前后的Name来看,中间位置的文本Sublime Text(UNREGISTERED)是始终不变的,对于变化的前半部分可以采用.*来进行匹配
#使用正则表达式的方式进行匹配 .*可以匹配任意个字符
win = app.window(title_re='.*Sublime Text.*')

这里的ClassName为使用class_name定位窗口时的参数
#class_name定位窗口
win=app.window(class_name="PX_WINDOW_CLASS")
#class_name_re定位窗口
app.window(class_name_re=".*WINDOW.*)
对于best_match定位可以先采用print_control_identifiers() 方法打印窗口及其子控件的标识符信息
app=Application(backend="uia").connect(process=21216)
win = app.window(title_re='.*Sublime Text .*')
win.print_control_identifiers()
打印内容为
Dialog - 'untitled • - Sublime Text (UNREGISTERED)' (L449, T946, R1587, B1933)
['untitled • - Sublime Text (UNREGISTERED)', 'Dialog', 'untitled • - Sublime Text (UNREGISTERED)Dialog']
child_window(title="untitled • - Sublime Text (UNREGISTERED)", control_type="Window")
|
| ScrollBar - '' (L460, T921, R560, B931)
| ['ScrollBar', 'ScrollBar0', 'ScrollBar1']
| |
| | Button - '左移一列' (L0, T0, R0, B0)
| | ['Button', '左移一列', '左移一列Button', 'Button0', 'Button1']
| | child_window(title="左移一列", auto_id="UpButton", control_type="Button")
| |
| | Thumb - '位置' (L0, T0, R0, B0)
| | ['位置', 'Thumb', '位置Thumb', '位置0', '位置1', 'Thumb0', 'Thumb1', '位置Thumb0', '位置Thumb1']
| | child_window(title="位置", auto_id="ScrollbarThumb", control_type="Thumb")
| |
| | Button - '右移一列' (L0, T0, R0, B0)
| | ['右移一列Button', 'Button2', '右移一列']
| | child_window(title="右移一列", auto_id="DownButton", control_type="Button")
|
| ScrollBar - '' (L360, T1021, R370, B1121)
| ['ScrollBar2']
| |
| | Button - '上一行' (L0, T0, R0, B0)
| | ['上一行', '上一行Button', 'Button3']
| | child_window(title="上一行", auto_id="UpButton", control_type="Button")
| |
| | Thumb - '位置' (L0, T0, R0, B0)
| | ['位置2', 'Thumb2', '位置Thumb2']
| | child_window(title="位置", auto_id="ScrollbarThumb", control_type="Thumb")
| |
| | Button - '下一行' (L0, T0, R0, B0)
| | ['Button4', '下一行Button', '下一行']
| | child_window(title="下一行", auto_id="DownButton", control_type="Button")
|
| TitleBar - '' (L484, T949, R1576, B991)
| ['TitleBar']
| |
| | Menu - '系统' (L460, T957, R493, B990)
| | ['Menu', '系统', '系统Menu', '系统0', '系统1', 'Menu0', 'Menu1']
| | child_window(title="系统", auto_id="MenuBar", control_type="MenuBar")
| | |
| | | MenuItem - '系统' (L460, T957, R493, B990)
| | | ['系统2', '系统MenuItem', 'MenuItem', 'MenuItem0', 'MenuItem1']
| | | child_window(title="系统", control_type="MenuItem")
| |
| | Button - '最小化' (L1365, T947, R1436, B991)
| | ['最小化Button', 'Button5', '最小化']
| | child_window(title="最小化", control_type="Button")
| |
| | Button - '最大化' (L1436, T947, R1506, B991)
| | ['最大化Button', 'Button6', '最大化']
| | child_window(title="最大化", control_type="Button")
| |
| | Button - '关闭' (L1506, T947, R1577, B991)
| | ['关闭', '关闭Button', 'Button7']
| | child_window(title="关闭", control_type="Button")
|
| Menu - '应用程序' (L460, T991, R1576, B1020)
| ['应用程序', 'Menu2', '应用程序Menu']
| child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
| |
| | MenuItem - 'File' (L460, T991, R512, B1020)
| | ['MenuItem2', 'File', 'FileMenuItem']
| | child_window(title="File", control_type="MenuItem")
| |
| | MenuItem - 'Edit' (L512, T991, R568, B1020)
| | ['MenuItem3', 'EditMenuItem', 'Edit']
| | child_window(title="Edit", control_type="MenuItem")
| |
| | MenuItem - 'Selection' (L568, T991, R668, B1020)
| | ['SelectionMenuItem', 'MenuItem4', 'Selection']
| | child_window(title="Selection", control_type="MenuItem")
| |
| | MenuItem - 'Find' (L668, T991, R728, B1020)
| | ['MenuItem5', 'FindMenuItem', 'Find']
| | child_window(title="Find", control_type="MenuItem")
| |
| | MenuItem - 'View' (L728, T991, R791, B1020)
| | ['View', 'ViewMenuItem', 'MenuItem6']
| | child_window(title="View", control_type="MenuItem")
| |
| | MenuItem - 'Goto' (L791, T991, R855, B1020)
| | ['GotoMenuItem', 'Goto', 'MenuItem7']
| | child_window(title="Goto", control_type="MenuItem")
| |
| | MenuItem - 'Tools' (L855, T991, R922, B1020)
| | ['ToolsMenuItem', 'Tools', 'MenuItem8']
| | child_window(title="Tools", control_type="MenuItem")
| |
| | MenuItem - 'Project' (L922, T991, R1004, B1020)
| | ['Project', 'ProjectMenuItem', 'MenuItem9']
| | child_window(title="Project", control_type="MenuItem")
| |
| | MenuItem - 'Preferences' (L1004, T991, R1125, B1020)
| | ['MenuItem10', 'Preferences', 'PreferencesMenuItem']
| | child_window(title="Preferences", control_type="MenuItem")
| |
| | MenuItem - 'Help' (L1125, T991, R1188, B1020)
| | ['MenuItem11', 'Help', 'HelpMenuItem']
| | child_window(title="Help", control_type="MenuItem")
注意:print_control_identifiers()打印结果可能与UISpy中的有差异,以打印结果为准
在其中在[]中的就是各元素的best_match定位的参数,选择填一个即可
#['untitled • - Sublime Text (UNREGISTERED)', 'Dialog', 'untitled • - Sublime Text (UNREGISTERED)Dialog']
#选一个定位即可
win=app.window(best_match='Dialog')
win=app.window(best_match='untitled • - Sublime Text (UNREGISTERED)Dialog')
win=app.window(best_match='untitled • - Sublime Text (UNREGISTERED)')
通过动态解析对象属性定位
函数原型:app.“best_match”名称
win=app.Dialog
#相当于下面的写法
win=app.window(best_match='Dialog')
#存在中文,空格或者其他特殊字符的可以采用以下的写法 ----推荐写法
win = app['untitled • - Sublime Text (UNREGISTERED)']
#上面写法等价于
win = app.window(best_match="untitled • - Sublime Text (UNREGISTERED)")
6.窗口操作
import time
from pywinauto import Application
#打开窗口
app=Application("uia").connect(process=32288)
#定位窗口
win=app.window(class_name="PX_WINDOW_CLASS")
win.wait("exists")
#对窗口进行操作
time.sleep(10)
#关闭窗口--关闭应用程序
win.close()
#窗口的最大化与最小化
win.maximize()
print("max result:",win.is_maximized)
time.sleep(3)
win.minimize()
print("min result:",win.is_minimized())
#窗口还原
win.restore()
7.定位控件
控件分类
桌面应用程序客户端的控件和层级结构是GUI自动化测试的基础。理解控件的分类、特性和层级关系, 能够帮助测试人员更高效地定位和操作控件,实现自动化测试。
在UISpy中可查看控件的类型

常见控件类型:

在使用 pywinauto 进行自动化操作时,程序启动后,首先需要定位到窗口,然后定位到控件(自动化流程的基础),然后对控件进行操作(自动化任务的核心)
定位控件,可以借助print_control_identifiers() 方法,打印窗口及其子控件的标识符信息
best_match定位
# 创建Application对象,连接到正在运行的Sublime Text进程
app = Application(backend='uia').connect(process=38544)
# 获取与Sublime Text相关的窗口对象,使用正则表达式匹配窗口标题
win = app.window(title_re='.*Sublime Text.*')
# 等待窗口变为可见状态,确保窗口已经加载完成
win.wait("visible")
# 通过窗口的标题获取菜单对象
# Menu - '应用程序' (L-31989, T-31989, R-29829, B-31960)
# | ['应用程序', 'Menu2', '应用程序Menu']
# proc=win.Menu2 --这种方式不建议,若.后面内容不含有空格即可 best_match有中文或标点会导致代码错误
menu = win['应用程序']
# 打印菜单项的列表,查看菜单中包含的所有选项
print(menu.items())
child_window定位
child_window() 和定位窗口的方法 window() 参数一样,可以通过标题或者类名进行精确匹 配、模糊匹配等,扩展几个额外的参数

可以选择部分参数进行控件定位,也可以组合参数进行控件定位,只需要准确定位到唯一控件即可
#参数只有一个,要保证能唯一定位到一个控件
proc=win.child_window(title="应用程序")
proc.wait("visible")
当UISpy中的显示信息与print_control_identifiers()打印信息不一致时,以打印的为准,输出没有的不能使用
win.child_window(class_name="ScrollBar",control_type="Pane",found_index=0).click_input() --ERROR
proc=win.child_window(class_name="ScrollBar",found_index=0)
#打印控件及子控件信息
proc.print_control_identifiers()
在实际中,常常使用打印结果中的信息进行定位

直接使用这些打印信息进行控件的定位
这里可以看到有两个类型为ScrollBar的控件,可以配合found_index进行定位
win.child_window(auto_id="ScrollBar",found_index=0)
如果控件不唯一,定位控件不一定报错,但对控件操作一定会报错(如click_input()点击)
父子关系定位
控件之间存在父子关系,可通过 children() 方法获取控件的子类,也可通过 parent() 方法获取控件的父类
#通过子控件来找父控件
# file_menu=win.child_window(title="File",control_type="MenuItem")
# file_parent=file_menu.parent()
# print(file_parent)
#根据父控件来找所有的子控件
# proc_menu=win.child_window(title="应用程序", auto_id="MenuBar", control_type="MenuBar")
# print(len(proc_menu.children()))
8.等待
pywinauto中提供了等待机制,来等待控件达到一种状态
这里先通过一个示例来说明下没有等待时会出现的问题
from pywinauto import Application
app = Application(backend='uia').connect(process=24600)
win = app.window(title_re='.*Sublime Text.*')
#添加等待
#win.wait('exists')
#最小化
win.minimize()
print("is_minimized:",win.is_minimized())
win.maximize()
print("is_maximized:",win.is_maximized())
win.close()
这时运行程序的话,就会直接报错

这是因为GUI应用程序行为通常不稳定,脚本需要等待,直到出现新窗口或关闭/隐藏现有窗口
wait/wait_not
函数原型:
#wait 是等待处于某种状态,而 wait_not 是等待不处于某种状态
wait(self, wait_for, timeout=None, retry_interval=None)
wait_not(self, wait_for_not, timeout=None, retry_interval=None)
参数解释:
wait_for :表示选择的控件状态
exists :表示控件是一个有效的句柄
visible :表示控件不隐藏,可以看到
enable :表示控件未被禁用,可操作
ready :表示控件可见且已启用
active :表示控件处于活动状态
timeout :表示超时
retry_interval :表示重试时间间隔,单位为秒s
app=Application("uia").connect(process=13344)
win=app.window(title="计算器")
win.wait("visible")
#win.print_control_identifiers()
#启用的按钮
enable_btn=win.child_window(title="记忆减法",auto_id="MemMinus",control_type="Button")
#未启用的按钮
disable_btn=win.child_window(title="清除所有记忆",auto_id="ClearMemoryButton",control_type="Button")
enable_btn.wait("enabled") #代码执行通过
#disable_btn.wait("enabled") #代码执行失败
disable_btn.wait_not("enabled")
sublime_app = Application(backend="uia").connect(process=9388)
sublime_win = sublime_app.window(title_re=".*Sublime Text.*")
app = Application(backend="uia").connect(process=17892)
win = app.window(title="计算器")
win.set_focus()
win.child_window(title="一", auto_id="num1Button",
control_type="Button").click_input()
win.wait("active") #等待成功
sublime_win.wait("active") #等待失败
使用建议:
- 最小化时需要可将等待状态改为 "exists"
- 应用程序界面在桌面可见时等待状态可以设置为 "visible"
- 若按钮置灰状态,可以设置按钮为 disabled 状态
- 当输入项不为空时,按钮高亮,此时为 enabled 状态
- active需要先操作应用程序使得焦点设置在该窗 口上或者配合 set_focus 来使用
is_visible() 用于检查元素是否可见, is_enabled() 用于检查元素是否启用
wait_until
函数原型:
wait_until(timeout,retry_interval,func,value=True,op=operator.eq,*args, **kwargs)
参数解释:
- Timeout :超时时间
- retry_interval :重试时间
- func :执行的函数
- value :比较的值
i=0
def work():
global i
i += 1
print("当前i的值为",i)
return i
#等待work返回的结果为5,继续往下执行
wait_until(10,1,work,5)
print("等待通过")
wait_cpu_usage_lower()
函数作用:等待该进程的cpu的使用率低于某个阀值
参数:
threshold:该进程cpu占用率(必填)
timeout:超时时间
retry_interval:重试时间间隔

1974

被折叠的 条评论
为什么被折叠?



