当前位置:   article > 正文

Windows软件UI自动化测试之UiAutomation

uiautomation

一、背景

        最近在研究基于windows的UI自动化测试,通过自动化来解决重复、枯燥的人工点点点,目前支持Windows平台的UI自动化工具或框架比较多,比如:Autoit、pywinauto、UIautomation、airtest 等等,这里我主要介绍UIautomation框架,它是由国人yinkaisheng开发实现的。

        为更加方便使用,我对UiAutomation进行了二次封装,并对每个接口进行了详细的说明,方便初学者使用,因为做自动化测试的有可能是测试人员,本身编码能力不是很强,同时受测试交付压力,他们需要的是简单快捷的实现用例自动化测试,快速完成测试交付。源代码在文章末尾,关注我即可获取。

二、UIautomation框架介绍

        UiAutomation封装了微软Windows UIAutomation API,支持自动化Win32,MFC,WPF,Modern UI(Metro UI), Qt, IE, Firefox等UI框架,最新版uiautomation2.0目前只支持Python 3版本,依赖comtypes和typing这两个包,但不要使用Python3.7.6和3.8.1这两个版本,因为comtypes在这两个版本中不能正常工作。

        UiAutomation支持在Windows XP SP3或更高版本的Windows桌面系统上运行。如果是Windows XP系统,请确保系统目录有这个文件:UIAutomationCore.dll。如果没有,需要安装补丁 KB971513 才能支持UIAutomtion.在Windows 7或更高版本Windows系统上使用UiAutomation时,要以管理员权限运行Python,否则UiAutomation运行时很多函数可能会执行失败或抛出异常。

三、UiAutomation安装

 pip install uiautomation

github下载连接:https://github.com/yinkaisheng/Python-UIAutomation-for-Windows 

四、界面元素定位工具 

         因为我们要对UI界面做一些操作,比如:点击按钮、下拉列表、操作菜单等等,所以要对UI界面元素进行定位,只有在准确的获取到元素的位置后,才能进行像点击、输入文本的操作,常见的界面元素定位工具有:spy++、Inspect.exe、UIspy.exe,我这里推荐使用Inspect.exe。

下载链接:https://pan.baidu.com/s/1JmkIFdiw_r7PO8MuCXNp7Q 
提取码:0qlw

五、常用控件

  • Control: 控制类型父类
  • WindowControl: 窗口控件类
  • PaneControl: 窗格控件类型
  • ButtonControl: 按钮控制类型
  • CheckBoxControl: 复选框控件类型
  • ComboBoxControl: 组合框控件类型
  • EditControl: 编辑控件类型
  • ListControl: 列表控件类型
  • ListItemControl: ListItem 控件类型
  • MenuControl: 菜单控制类型
  • MenuBarControl: 菜单栏控件类型
  • MenuItemControl: 菜单项控件类型
  • ScrollBarControl: 滚动条控件类型
  • SliderControl: 滑块控制类型
  • TabControl: 选项卡控件类型
  • TabItemControl: TabItem 控件类型
  • TableControl: 表控件类型
  • TextControl: 文本控件类型
  • TitleBarControl: 标题栏控件类型
  • ToolBarControl: 工具栏控件类型
  • ToolTipControl: 工具提示控件类型
  • TreeControl: 树控件类型
  • TreeItemControl: 树项控件类型
  • AppBarControlAppBar: 控件类型
  • CalendarControl: 日历控件类型
  • DataGridControl: 数据网格控件类型
  • GroupControl: 群控类型
  • HeaderControl: 标题控件类型
  • HeaderItemControl: HeaderItem: 控件类型
  • HyperlinkControl: 超链接控制类型
  • ImageControl: 图像控制类型
  • DataItemControl: 数据项控件类型
  • DocumentControl: 文件控制类型
  • ProgressBarControl: ProgressBar: 控件类型
  • RadioButtonControl: 单选按钮控件类型
  • SemanticZoomControl: SemanticZoom控制类型
  • SeparatorControl: 分离器控制类型
  • SpinnerControl: 微调控制类型
  • SplitButtonControl: 拆分按钮控件类型
  • StatusBarControl: 状态栏控件类型
  • ThumbControl: 拇指控制类型

控件支持的参数如下: 

  • searchFromControl: 从哪个控件开始查找,如果为None,从根节点Desktop开始查找
  • searchDepth: 搜索深度
  • searchInterval: 搜索间隔
  • foundIndex: 搜索到的满足搜索条件的控件索引,索引从1开始
  • Name: 控件名字
  • SubName: 控件部分名字
  • RegexName: 使用re.match匹配符合正则表达式的名字,Name,SubName,RegexName只能使用一个,不能同时使用
  • ClassName: 类名字
  • AutomationId: 控件AutomationId
  • ControlType: 控件类型
  • Depth: 控件相对于searchFromControl的精确深度
  • Compare: 自定义比较函数function(control: Control, depth: int)->bool

 六、二次封装使用

        为更加方便使用,我对UiAutomation进行了二次封装,并对每个接口进行了详细的说明,方便初学者使用,因为做自动化测试的有可能是测试人员,本身编码能力不是很强,同时受测试交付压力,他们需要的是简单快捷的实现用例自动化测试,快速完成测试交付。

  1. # -*- coding: utf-8 -*-
  2. # @Time : 2022/10/28 21:36
  3. # @Author : 十年
  4. # @Site : https://gitee.com/chshao/aiplotest
  5. # @CSDN : https://blog.csdn.net/m0_37576542?type=blog
  6. # @File : AiUiAutomation.py
  7. # @Description : PC应用软件的UI自动化测试模块,实现了对UI常用操作的封装
  8. import os
  9. import time
  10. import subprocess
  11. import uiautomation as ui
  12. class AiUiAutomation(object):
  13. @staticmethod
  14. def setLogPath(savePath):
  15. if not os.path.exists(savePath):
  16. os.makedirs(savePath)
  17. fileName = "AutomationLog_%s.txt" % time.strftime("%Y_%m_%d_%H_%M_%S")
  18. ui.Logger.SetLogFile(os.path.join(savePath, fileName))
  19. @staticmethod
  20. def openApplication(app, Name=None, ClassName=None, searchDepth=1, timeout=5):
  21. """
  22. @summary: 打开应用
  23. @param app: 应用名称或exe文件的绝对路径
  24. @param Name: 窗体的Name属性,可通过inspect工具查看
  25. @param ClassName: 窗体的ClassName属性,可通过inspect工具查看
  26. @param searchDepth: 搜索深度
  27. @param timeout: 超时时间
  28. @return: (True, ) or (False, "打开失败")
  29. @attention: 非系统自带应用最好通过exe绝对路径打开
  30. """
  31. if os.path.isabs(app):
  32. if not os.path.exists(app):
  33. return False, "不存在[%s]" % app
  34. subprocess.Popen(app)
  35. window = AiUiAutomation.WindowControl(Name, ClassName, searchDepth, timeout)
  36. if window is None:
  37. return False, "打开失败"
  38. return True, window
  39. @staticmethod
  40. def closeApplication(exeName):
  41. os.popen('taskkill /F /IM {}'.format(exeName))
  42. return True, "Ok"
  43. @staticmethod
  44. def WindowControl(Name, ClassName=None, searchDepth: int = 0xFFFFFFFF, timeout=5):
  45. """
  46. @summary: 获取窗体对象
  47. @param Name: 窗口Name属性值
  48. @param ClassName: 窗口ClassName属性值
  49. @param searchDepth: 搜索深度
  50. @param timeout: 超时时间
  51. @return:
  52. """
  53. searchProperties = {}
  54. if Name is not None:
  55. searchProperties.update({"Name": Name})
  56. if ClassName is not None:
  57. searchProperties.update({"ClassName": ClassName})
  58. window = ui.WindowControl(searchDepth=searchDepth, **searchProperties)
  59. # 可以判断window是否存在
  60. if ui.WaitForExist(window, timeout):
  61. return window
  62. return None
  63. @staticmethod
  64. def setWindowActive(window: ui.WindowControl):
  65. """
  66. @summary: 激活窗口
  67. @param window: 待激活窗口对象
  68. @return: True or False
  69. """
  70. return window.SetActive()
  71. @staticmethod
  72. def setWindowTopMost(window: ui.WindowControl):
  73. """
  74. @summary: 将窗口置顶
  75. @param window: 待置顶窗口对象
  76. @return: True or False
  77. """
  78. return window.SetTopmost()
  79. @staticmethod
  80. def moveWindowToCenter(window: ui.WindowControl):
  81. """
  82. @summary: 将窗口居中显示
  83. @param window: 待居中显示的窗口对象
  84. @return: True or False
  85. """
  86. return window.MoveToCenter()
  87. @staticmethod
  88. def checkWindowExist(window: ui.WindowControl, timeout):
  89. """
  90. @summary: 检查窗口是否存在
  91. @param window: 待检查的窗口对象
  92. @param timeout: 最大检查时长
  93. @return: True or False
  94. """
  95. return window.Exists(timeout)
  96. @staticmethod
  97. def closeWindow(window: ui.WindowControl):
  98. """
  99. @summary: 关闭窗口
  100. @param window: 待关闭的窗口对象
  101. @return: True or False
  102. """
  103. pattern = window.GetWindowPattern()
  104. if pattern is None:
  105. return False
  106. return pattern.Close()
  107. @staticmethod
  108. def showWindowMax(window: ui.WindowControl):
  109. """
  110. @summary: 窗口最大化显示
  111. @param window: 窗口对象
  112. @return: True or False
  113. """
  114. if window.IsMaximize():
  115. return True
  116. return window.Maximize()
  117. @staticmethod
  118. def showWindowMin(window: ui.WindowControl):
  119. """
  120. @summary: 窗口最小化显示
  121. @param window: 窗口对象
  122. @return: True or False
  123. """
  124. if window.IsMinimize():
  125. return True
  126. return window.Minimize()
  127. @staticmethod
  128. def switchWindow(window: ui.WindowControl):
  129. """
  130. @summary: 切换到目标窗口
  131. @param window: 目标窗口对象
  132. @return:
  133. """
  134. if not window.IsTopLevel():
  135. return False
  136. window.SwitchToThisWindow()
  137. return True
  138. @staticmethod
  139. def MenuItemControl(parent: ui.WindowControl, SubName=None, Name=None):
  140. """
  141. @summary: 获取菜单选项控件
  142. @param parent: 父窗体
  143. @param SubName: 控件部分名字
  144. @param Name: 控件名字,SubName、Name只能使用一个,不能同时使用
  145. @return:
  146. @attention: 有时候通过Name可能获取不到控件,可以尝试通过SubName获取
  147. """
  148. if SubName is not None:
  149. return parent.MenuItemControl(SubName=SubName)
  150. if Name is not None:
  151. return parent.MenuItemControl(Name=Name)
  152. @staticmethod
  153. def clickMenuItemBySubName(parent: ui.WindowControl, SubName, waitTime=1, useLoc=False):
  154. """
  155. @summary: 根据控件名称部分内容点击菜单选项
  156. @param parent: 菜单所在窗口
  157. @param SubName: 菜单控件名称部分内容
  158. @param waitTime: 点击后等待时长
  159. @param useLoc: 是否转化成坐标进行点击,之所以转成坐标点击是因为有时候点击名称会失败
  160. @return:
  161. """
  162. menuItem = parent.MenuItemControl(SubName=SubName)
  163. if useLoc:
  164. x = menuItem.BoundingRectangle.xcenter()
  165. y = menuItem.BoundingRectangle.ycenter()
  166. AiUiAutomation.click(x, y, waitTime)
  167. else:
  168. menuItem.Click(waitTime=waitTime)
  169. @staticmethod
  170. def clickMenuItemByName(parent: ui.WindowControl, Name, waitTime=1, useLoc=False):
  171. """
  172. @summary: 根据控件名称点击菜单选项
  173. @param parent: 菜单所在窗口
  174. @param Name: 菜单控件名称
  175. @param waitTime: 点击后等待时长
  176. @param useLoc: 是否转化成坐标进行点击,之所以转成坐标点击是因为有时候点击名称会失败
  177. @return:
  178. """
  179. menuItem = parent.MenuItemControl(Name=Name)
  180. if useLoc:
  181. x = menuItem.BoundingRectangle.xcenter()
  182. y = menuItem.BoundingRectangle.ycenter()
  183. AiUiAutomation.click(x, y, waitTime)
  184. else:
  185. menuItem.Click(waitTime=waitTime)
  186. @staticmethod
  187. def getControlRectCenter(control: ui.Control):
  188. """
  189. @summary: 获取控件的中心点坐标
  190. @param control: 目标控件
  191. @return: (x, y)
  192. """
  193. x = control.BoundingRectangle.xcenter()
  194. y = control.BoundingRectangle.ycenter()
  195. return x, y
  196. @staticmethod
  197. def takeScreenshots(savePath, control=None):
  198. """
  199. @summary: 截图并保存
  200. @param savePath: 保存文件路径
  201. @param control: 控件对象,默认全屏截图
  202. @return:
  203. """
  204. if control is None:
  205. control = ui.GetRootControl()
  206. if not isinstance(control, ui.PaneControl):
  207. return False, "参数control类型错误"
  208. control.CaptureToImage(savePath)
  209. # bitmap = ui.Bitmap.FromControl(control, startX, startY, width, height)
  210. # bitmap.ToFile(savePath)
  211. return True, savePath
  212. @staticmethod
  213. def showDesktop():
  214. """
  215. @summary: Show Desktop by pressing win + d
  216. """
  217. ui.SendKeys('{Win}d')
  218. @staticmethod
  219. def click(x, y, waitTime=0.5):
  220. """
  221. @summary: 鼠标点击指定坐标
  222. @param x: 横坐标x值,int类型
  223. @param y: 纵坐标y值,int类型
  224. @param waitTime: 点击后等待时间
  225. @return:
  226. """
  227. ui.Click(x, y, waitTime)
  228. @staticmethod
  229. def rightClick(x, y, waitTime=0.5):
  230. """
  231. @summary: 鼠标右键单击指定坐标
  232. @param x: 横坐标x值,int类型
  233. @param y: 纵坐标y值,int类型
  234. @param waitTime: 点击后等待时间
  235. @return:
  236. """
  237. ui.RightClick(x, y, waitTime)
  238. @staticmethod
  239. def dragDrop(startX, startY, endX, endY, moveSpeed=1, waitTime=0.5):
  240. """
  241. @summary: 鼠标拖拽(鼠标从(startX,startY)位置按下鼠标拖动到(endX,endY)位置)
  242. @param startX: 起始位置X坐标
  243. @param startY: 起始位置Y坐标
  244. @param endX: 终点位置X坐标
  245. @param endY: 终点位置Y坐标
  246. @param moveSpeed: 拖拽速度
  247. @param waitTime: 拖拽完成后等待时间
  248. @return:
  249. """
  250. ui.PressMouse(startX, startY, 0.05)
  251. ui.MoveTo(endX, endY, moveSpeed, 0.05)
  252. ui.ReleaseMouse(waitTime)
  253. @staticmethod
  254. def ComboBoxControl(parent: ui.WindowControl, Name=None, AutomationId=None, timeout=3):
  255. """
  256. @summary: 获取下拉列表控件对象
  257. @param parent: 所在窗口
  258. @param Name: 下拉列表Name属性值
  259. @param AutomationId: 下拉列表AutomationId值
  260. @param timeout: 超时时间
  261. @return:
  262. """
  263. searchProperties = {}
  264. if Name is not None:
  265. searchProperties.update({"Name": Name})
  266. if AutomationId is not None:
  267. searchProperties.update({"AutomationId": AutomationId})
  268. combox = parent.ComboBoxControl(**searchProperties)
  269. # 可以判断combox是否存在
  270. if combox.Exists(timeout):
  271. return combox
  272. return None
  273. @staticmethod
  274. def selectComboBoxItem(combo: ui.ComboBoxControl, itemName, waitTime=0.5):
  275. """
  276. @summary: 选择下拉框选项
  277. @param combo: 待操作的下拉框控件对象
  278. @param itemName: 待选择的选项名称
  279. @param waitTime: 选择后的等待时长
  280. @return:
  281. @attention: 此方法在有些下拉框列表可能不生效, 比如用老的QT版本开发的应用.
  282. """
  283. return combo.Select(itemName=itemName, waitTime=waitTime)
  284. @staticmethod
  285. def ListControl(parent: ui.WindowControl, Name=None, AutomationId=None, timeout=3):
  286. """
  287. @summary: 获取列表控件对象
  288. @param parent: 所在窗口
  289. @param Name: 列表Name属性值
  290. @param AutomationId: 列表AutomationId值
  291. @param timeout: 超时时间
  292. @return:
  293. """
  294. searchProperties = {}
  295. if Name is not None:
  296. searchProperties.update({"Name": Name})
  297. if AutomationId is not None:
  298. searchProperties.update({"AutomationId": AutomationId})
  299. listControl = parent.ListControl(**searchProperties)
  300. # 可以判断列表是否存在
  301. if listControl.Exists(timeout):
  302. return listControl
  303. return None
  304. @staticmethod
  305. def ListItemControl(parent: ui.ListControl, Name=None, SubName=None, timeout=3):
  306. """
  307. @summary: 获取列表item控件对象
  308. @param parent: 父列表控件对象
  309. @param Name: Name属性值
  310. @param SubName: SubName值
  311. @param timeout: 超时时间
  312. @return:
  313. """
  314. if Name is not None:
  315. itemControl = parent.ListItemControl(Name=Name)
  316. if itemControl.Exists(timeout):
  317. return itemControl
  318. itemControl = parent.ListItemControl(SubName=SubName)
  319. if itemControl.Exists(timeout):
  320. return itemControl
  321. @staticmethod
  322. def selectListItemByName(parent: ui.ListControl, Name=None, SubName=None, waitTime=0.5, isScroll=True):
  323. """
  324. @summary: 根据名称选择列表item
  325. @param parent: 父列表控件对象
  326. @param Name: 列表item名称完整内容
  327. @param SubName: SubName值,列表item名称部分内容
  328. @param waitTime: 点击完成后等待时长
  329. @param isScroll: 是否允许滚动查找到目标item后再点击
  330. @return:
  331. """
  332. itemControl = AiUiAutomation.ListItemControl(parent, Name, SubName)
  333. if itemControl is None:
  334. return False
  335. if isScroll:
  336. itemControl.GetScrollItemPattern().ScrollIntoView()
  337. itemControl.Click(waitTime=waitTime)
  338. return True
  339. @staticmethod
  340. def ButtonControl(parent: ui.WindowControl, Name=None, timeout=3):
  341. """
  342. @summary: 获取按钮控件对象
  343. @param parent: 按钮所在窗口对象
  344. @param Name: 按钮名称
  345. @param timeout: 超时时间
  346. @return:
  347. """
  348. button = parent.ButtonControl(Name=Name)
  349. if button.Exists(timeout):
  350. return button
  351. return None
  352. @staticmethod
  353. def clickButton(button: ui.ButtonControl, waitTime=0.5):
  354. """
  355. @summary: 点击按钮
  356. @param button: 待点击的按钮对象
  357. @param waitTime: 点击后等待时长
  358. @return:
  359. """
  360. if button is None:
  361. return False
  362. button.Click(waitTime=waitTime)
  363. return True
  364. @staticmethod
  365. def CheckBoxControl(parent: ui.Control, Name=None, AutomationId=None, timeout=3):
  366. """
  367. @summary: 获取复选框控件对象
  368. @param parent: 复选框所在窗口对象
  369. @param Name: 复选框名称
  370. @param AutomationId: 复选框id
  371. @param timeout: 超时时间
  372. @return:
  373. """
  374. searchProperties = {}
  375. if Name is not None:
  376. searchProperties.update({"Name": Name})
  377. if AutomationId is not None:
  378. searchProperties.update({"AutomationId": AutomationId})
  379. checkBox = parent.CheckBoxControl(**searchProperties)
  380. # 可以判断复选框是否存在
  381. if checkBox.Exists(timeout):
  382. return checkBox
  383. return None
  384. @staticmethod
  385. def selectCheckBox(checkBox: ui.CheckBoxControl, waitTime=0.5):
  386. """
  387. @summary: 选中复选框
  388. @param checkBox: 复选框对象
  389. @param waitTime: 等待时间
  390. @return:
  391. """
  392. if checkBox.GetTogglePattern().ToggleState == 0:
  393. checkBox.Click(waitTime=waitTime)
  394. if checkBox.GetTogglePattern().ToggleState == 1: # 1是选中状态,0是未选中状态
  395. return True
  396. return False
  397. @staticmethod
  398. def unSelectCheckBox(checkBox: ui.CheckBoxControl):
  399. """
  400. @summary: 取消勾选复选框
  401. @param checkBox: 复选框对象
  402. @return:
  403. """
  404. if checkBox.GetTogglePattern().ToggleState == 1:
  405. checkBox.Click()
  406. if checkBox.GetTogglePattern().ToggleState == 0:
  407. return True
  408. return False
  409. @staticmethod
  410. def ProgressBarControl(parent: ui.WindowControl, ClassName=None, Name=None, AutomationId=None, timeout=3):
  411. searchProperties = {}
  412. if ClassName is not None:
  413. searchProperties.update({"ClassName": ClassName})
  414. if Name is not None:
  415. searchProperties.update({"Name": Name})
  416. if AutomationId is not None:
  417. searchProperties.update({"AutomationId": AutomationId})
  418. progressBar = parent.ProgressBarControl(**searchProperties)
  419. # 可以判断复选框是否存在
  420. if progressBar.Exists(timeout):
  421. return progressBar
  422. return None
  423. @staticmethod
  424. def getProgressBarValue(progressBar: ui.ProgressBarControl):
  425. return int(progressBar.GetLegacyIAccessiblePattern().Value)
  426. @staticmethod
  427. def EditControl(parent: ui.WindowControl, ClassName=None, Name=None, AutomationId=None, timeout=3):
  428. searchProperties = {}
  429. if ClassName is not None:
  430. searchProperties.update({"ClassName": ClassName})
  431. if Name is not None:
  432. searchProperties.update({"Name": Name})
  433. if AutomationId is not None:
  434. searchProperties.update({"AutomationId": AutomationId})
  435. edit = parent.EditControl(**searchProperties)
  436. # 可以判断编辑框是否存在
  437. if edit.Exists(timeout):
  438. return edit
  439. return None
  440. @staticmethod
  441. def inputEditControlText(editControl: ui.EditControl, text):
  442. return editControl.GetValuePattern().SetValue(text)
  443. if __name__ == '__main__':
  444. app = r'D:\程序安装\Notepad++\notepad++.exe'
  445. AiUiAutomation.setLogPath(r'D:\report')
  446. sta, window = AiUiAutomation.openApplication(app, Name=None, ClassName="Notepad++")
  447. if sta:
  448. AiUiAutomation.clickMenuItemByName(window, Name="设置(T)")
  449. AiUiAutomation.clickMenuItemBySubName(window, SubName="首选项...", useLoc=True)
  450. firstChoiceWindow = AiUiAutomation.WindowControl("首选项")
  451. button = AiUiAutomation.ButtonControl(firstChoiceWindow, Name="关闭")
  452. AiUiAutomation.clickButton(button)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/476385
推荐阅读
相关标签
  

闽ICP备14008679号