赞
踩
最近在研究基于windows的UI自动化测试,通过自动化来解决重复、枯燥的人工点点点,目前支持Windows平台的UI自动化工具或框架比较多,比如:Autoit、pywinauto、UIautomation、airtest 等等,这里我主要介绍UIautomation框架,它是由国人yinkaisheng
开发实现的。
为更加方便使用,我对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运行时很多函数可能会执行失败或抛出异常。
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进行了二次封装,并对每个接口进行了详细的说明,方便初学者使用,因为做自动化测试的有可能是测试人员,本身编码能力不是很强,同时受测试交付压力,他们需要的是简单快捷的实现用例自动化测试,快速完成测试交付。
- # -*- coding: utf-8 -*-
- # @Time : 2022/10/28 21:36
- # @Author : 十年
- # @Site : https://gitee.com/chshao/aiplotest
- # @CSDN : https://blog.csdn.net/m0_37576542?type=blog
- # @File : AiUiAutomation.py
- # @Description : PC应用软件的UI自动化测试模块,实现了对UI常用操作的封装
- import os
- import time
- import subprocess
-
- import uiautomation as ui
-
-
- class AiUiAutomation(object):
-
- @staticmethod
- def setLogPath(savePath):
- if not os.path.exists(savePath):
- os.makedirs(savePath)
- fileName = "AutomationLog_%s.txt" % time.strftime("%Y_%m_%d_%H_%M_%S")
- ui.Logger.SetLogFile(os.path.join(savePath, fileName))
-
- @staticmethod
- def openApplication(app, Name=None, ClassName=None, searchDepth=1, timeout=5):
- """
- @summary: 打开应用
- @param app: 应用名称或exe文件的绝对路径
- @param Name: 窗体的Name属性,可通过inspect工具查看
- @param ClassName: 窗体的ClassName属性,可通过inspect工具查看
- @param searchDepth: 搜索深度
- @param timeout: 超时时间
- @return: (True, ) or (False, "打开失败")
- @attention: 非系统自带应用最好通过exe绝对路径打开
- """
- if os.path.isabs(app):
- if not os.path.exists(app):
- return False, "不存在[%s]" % app
- subprocess.Popen(app)
- window = AiUiAutomation.WindowControl(Name, ClassName, searchDepth, timeout)
- if window is None:
- return False, "打开失败"
- return True, window
-
- @staticmethod
- def closeApplication(exeName):
- os.popen('taskkill /F /IM {}'.format(exeName))
- return True, "Ok"
-
- @staticmethod
- def WindowControl(Name, ClassName=None, searchDepth: int = 0xFFFFFFFF, timeout=5):
- """
- @summary: 获取窗体对象
- @param Name: 窗口Name属性值
- @param ClassName: 窗口ClassName属性值
- @param searchDepth: 搜索深度
- @param timeout: 超时时间
- @return:
- """
- searchProperties = {}
- if Name is not None:
- searchProperties.update({"Name": Name})
- if ClassName is not None:
- searchProperties.update({"ClassName": ClassName})
- window = ui.WindowControl(searchDepth=searchDepth, **searchProperties)
- # 可以判断window是否存在
- if ui.WaitForExist(window, timeout):
- return window
- return None
-
- @staticmethod
- def setWindowActive(window: ui.WindowControl):
- """
- @summary: 激活窗口
- @param window: 待激活窗口对象
- @return: True or False
- """
- return window.SetActive()
-
- @staticmethod
- def setWindowTopMost(window: ui.WindowControl):
- """
- @summary: 将窗口置顶
- @param window: 待置顶窗口对象
- @return: True or False
- """
- return window.SetTopmost()
-
- @staticmethod
- def moveWindowToCenter(window: ui.WindowControl):
- """
- @summary: 将窗口居中显示
- @param window: 待居中显示的窗口对象
- @return: True or False
- """
- return window.MoveToCenter()
-
- @staticmethod
- def checkWindowExist(window: ui.WindowControl, timeout):
- """
- @summary: 检查窗口是否存在
- @param window: 待检查的窗口对象
- @param timeout: 最大检查时长
- @return: True or False
- """
- return window.Exists(timeout)
-
- @staticmethod
- def closeWindow(window: ui.WindowControl):
- """
- @summary: 关闭窗口
- @param window: 待关闭的窗口对象
- @return: True or False
- """
- pattern = window.GetWindowPattern()
- if pattern is None:
- return False
- return pattern.Close()
-
- @staticmethod
- def showWindowMax(window: ui.WindowControl):
- """
- @summary: 窗口最大化显示
- @param window: 窗口对象
- @return: True or False
- """
- if window.IsMaximize():
- return True
- return window.Maximize()
-
- @staticmethod
- def showWindowMin(window: ui.WindowControl):
- """
- @summary: 窗口最小化显示
- @param window: 窗口对象
- @return: True or False
- """
- if window.IsMinimize():
- return True
- return window.Minimize()
-
- @staticmethod
- def switchWindow(window: ui.WindowControl):
- """
- @summary: 切换到目标窗口
- @param window: 目标窗口对象
- @return:
- """
- if not window.IsTopLevel():
- return False
- window.SwitchToThisWindow()
- return True
-
- @staticmethod
- def MenuItemControl(parent: ui.WindowControl, SubName=None, Name=None):
- """
- @summary: 获取菜单选项控件
- @param parent: 父窗体
- @param SubName: 控件部分名字
- @param Name: 控件名字,SubName、Name只能使用一个,不能同时使用
- @return:
- @attention: 有时候通过Name可能获取不到控件,可以尝试通过SubName获取
- """
- if SubName is not None:
- return parent.MenuItemControl(SubName=SubName)
- if Name is not None:
- return parent.MenuItemControl(Name=Name)
-
- @staticmethod
- def clickMenuItemBySubName(parent: ui.WindowControl, SubName, waitTime=1, useLoc=False):
- """
- @summary: 根据控件名称部分内容点击菜单选项
- @param parent: 菜单所在窗口
- @param SubName: 菜单控件名称部分内容
- @param waitTime: 点击后等待时长
- @param useLoc: 是否转化成坐标进行点击,之所以转成坐标点击是因为有时候点击名称会失败
- @return:
- """
- menuItem = parent.MenuItemControl(SubName=SubName)
- if useLoc:
- x = menuItem.BoundingRectangle.xcenter()
- y = menuItem.BoundingRectangle.ycenter()
- AiUiAutomation.click(x, y, waitTime)
- else:
- menuItem.Click(waitTime=waitTime)
-
- @staticmethod
- def clickMenuItemByName(parent: ui.WindowControl, Name, waitTime=1, useLoc=False):
- """
- @summary: 根据控件名称点击菜单选项
- @param parent: 菜单所在窗口
- @param Name: 菜单控件名称
- @param waitTime: 点击后等待时长
- @param useLoc: 是否转化成坐标进行点击,之所以转成坐标点击是因为有时候点击名称会失败
- @return:
- """
- menuItem = parent.MenuItemControl(Name=Name)
- if useLoc:
- x = menuItem.BoundingRectangle.xcenter()
- y = menuItem.BoundingRectangle.ycenter()
- AiUiAutomation.click(x, y, waitTime)
- else:
- menuItem.Click(waitTime=waitTime)
-
- @staticmethod
- def getControlRectCenter(control: ui.Control):
- """
- @summary: 获取控件的中心点坐标
- @param control: 目标控件
- @return: (x, y)
- """
- x = control.BoundingRectangle.xcenter()
- y = control.BoundingRectangle.ycenter()
- return x, y
-
- @staticmethod
- def takeScreenshots(savePath, control=None):
- """
- @summary: 截图并保存
- @param savePath: 保存文件路径
- @param control: 控件对象,默认全屏截图
- @return:
- """
- if control is None:
- control = ui.GetRootControl()
- if not isinstance(control, ui.PaneControl):
- return False, "参数control类型错误"
- control.CaptureToImage(savePath)
- # bitmap = ui.Bitmap.FromControl(control, startX, startY, width, height)
- # bitmap.ToFile(savePath)
- return True, savePath
-
- @staticmethod
- def showDesktop():
- """
- @summary: Show Desktop by pressing win + d
- """
- ui.SendKeys('{Win}d')
-
- @staticmethod
- def click(x, y, waitTime=0.5):
- """
- @summary: 鼠标点击指定坐标
- @param x: 横坐标x值,int类型
- @param y: 纵坐标y值,int类型
- @param waitTime: 点击后等待时间
- @return:
- """
- ui.Click(x, y, waitTime)
-
- @staticmethod
- def rightClick(x, y, waitTime=0.5):
- """
- @summary: 鼠标右键单击指定坐标
- @param x: 横坐标x值,int类型
- @param y: 纵坐标y值,int类型
- @param waitTime: 点击后等待时间
- @return:
- """
- ui.RightClick(x, y, waitTime)
-
- @staticmethod
- def dragDrop(startX, startY, endX, endY, moveSpeed=1, waitTime=0.5):
- """
- @summary: 鼠标拖拽(鼠标从(startX,startY)位置按下鼠标拖动到(endX,endY)位置)
- @param startX: 起始位置X坐标
- @param startY: 起始位置Y坐标
- @param endX: 终点位置X坐标
- @param endY: 终点位置Y坐标
- @param moveSpeed: 拖拽速度
- @param waitTime: 拖拽完成后等待时间
- @return:
- """
- ui.PressMouse(startX, startY, 0.05)
- ui.MoveTo(endX, endY, moveSpeed, 0.05)
- ui.ReleaseMouse(waitTime)
-
- @staticmethod
- def ComboBoxControl(parent: ui.WindowControl, Name=None, AutomationId=None, timeout=3):
- """
- @summary: 获取下拉列表控件对象
- @param parent: 所在窗口
- @param Name: 下拉列表Name属性值
- @param AutomationId: 下拉列表AutomationId值
- @param timeout: 超时时间
- @return:
- """
- searchProperties = {}
- if Name is not None:
- searchProperties.update({"Name": Name})
- if AutomationId is not None:
- searchProperties.update({"AutomationId": AutomationId})
- combox = parent.ComboBoxControl(**searchProperties)
- # 可以判断combox是否存在
- if combox.Exists(timeout):
- return combox
- return None
-
- @staticmethod
- def selectComboBoxItem(combo: ui.ComboBoxControl, itemName, waitTime=0.5):
- """
- @summary: 选择下拉框选项
- @param combo: 待操作的下拉框控件对象
- @param itemName: 待选择的选项名称
- @param waitTime: 选择后的等待时长
- @return:
- @attention: 此方法在有些下拉框列表可能不生效, 比如用老的QT版本开发的应用.
- """
- return combo.Select(itemName=itemName, waitTime=waitTime)
-
- @staticmethod
- def ListControl(parent: ui.WindowControl, Name=None, AutomationId=None, timeout=3):
- """
- @summary: 获取列表控件对象
- @param parent: 所在窗口
- @param Name: 列表Name属性值
- @param AutomationId: 列表AutomationId值
- @param timeout: 超时时间
- @return:
- """
- searchProperties = {}
- if Name is not None:
- searchProperties.update({"Name": Name})
- if AutomationId is not None:
- searchProperties.update({"AutomationId": AutomationId})
- listControl = parent.ListControl(**searchProperties)
- # 可以判断列表是否存在
- if listControl.Exists(timeout):
- return listControl
- return None
-
- @staticmethod
- def ListItemControl(parent: ui.ListControl, Name=None, SubName=None, timeout=3):
- """
- @summary: 获取列表item控件对象
- @param parent: 父列表控件对象
- @param Name: Name属性值
- @param SubName: SubName值
- @param timeout: 超时时间
- @return:
- """
- if Name is not None:
- itemControl = parent.ListItemControl(Name=Name)
- if itemControl.Exists(timeout):
- return itemControl
- itemControl = parent.ListItemControl(SubName=SubName)
- if itemControl.Exists(timeout):
- return itemControl
-
- @staticmethod
- def selectListItemByName(parent: ui.ListControl, Name=None, SubName=None, waitTime=0.5, isScroll=True):
- """
- @summary: 根据名称选择列表item
- @param parent: 父列表控件对象
- @param Name: 列表item名称完整内容
- @param SubName: SubName值,列表item名称部分内容
- @param waitTime: 点击完成后等待时长
- @param isScroll: 是否允许滚动查找到目标item后再点击
- @return:
- """
- itemControl = AiUiAutomation.ListItemControl(parent, Name, SubName)
- if itemControl is None:
- return False
- if isScroll:
- itemControl.GetScrollItemPattern().ScrollIntoView()
- itemControl.Click(waitTime=waitTime)
- return True
-
- @staticmethod
- def ButtonControl(parent: ui.WindowControl, Name=None, timeout=3):
- """
- @summary: 获取按钮控件对象
- @param parent: 按钮所在窗口对象
- @param Name: 按钮名称
- @param timeout: 超时时间
- @return:
- """
- button = parent.ButtonControl(Name=Name)
- if button.Exists(timeout):
- return button
- return None
-
- @staticmethod
- def clickButton(button: ui.ButtonControl, waitTime=0.5):
- """
- @summary: 点击按钮
- @param button: 待点击的按钮对象
- @param waitTime: 点击后等待时长
- @return:
- """
- if button is None:
- return False
- button.Click(waitTime=waitTime)
- return True
-
- @staticmethod
- def CheckBoxControl(parent: ui.Control, Name=None, AutomationId=None, timeout=3):
- """
- @summary: 获取复选框控件对象
- @param parent: 复选框所在窗口对象
- @param Name: 复选框名称
- @param AutomationId: 复选框id
- @param timeout: 超时时间
- @return:
- """
- searchProperties = {}
- if Name is not None:
- searchProperties.update({"Name": Name})
- if AutomationId is not None:
- searchProperties.update({"AutomationId": AutomationId})
- checkBox = parent.CheckBoxControl(**searchProperties)
- # 可以判断复选框是否存在
- if checkBox.Exists(timeout):
- return checkBox
- return None
-
- @staticmethod
- def selectCheckBox(checkBox: ui.CheckBoxControl, waitTime=0.5):
- """
- @summary: 选中复选框
- @param checkBox: 复选框对象
- @param waitTime: 等待时间
- @return:
- """
- if checkBox.GetTogglePattern().ToggleState == 0:
- checkBox.Click(waitTime=waitTime)
- if checkBox.GetTogglePattern().ToggleState == 1: # 1是选中状态,0是未选中状态
- return True
- return False
-
- @staticmethod
- def unSelectCheckBox(checkBox: ui.CheckBoxControl):
- """
- @summary: 取消勾选复选框
- @param checkBox: 复选框对象
- @return:
- """
- if checkBox.GetTogglePattern().ToggleState == 1:
- checkBox.Click()
- if checkBox.GetTogglePattern().ToggleState == 0:
- return True
- return False
-
- @staticmethod
- def ProgressBarControl(parent: ui.WindowControl, ClassName=None, Name=None, AutomationId=None, timeout=3):
- searchProperties = {}
- if ClassName is not None:
- searchProperties.update({"ClassName": ClassName})
- if Name is not None:
- searchProperties.update({"Name": Name})
- if AutomationId is not None:
- searchProperties.update({"AutomationId": AutomationId})
- progressBar = parent.ProgressBarControl(**searchProperties)
- # 可以判断复选框是否存在
- if progressBar.Exists(timeout):
- return progressBar
- return None
-
- @staticmethod
- def getProgressBarValue(progressBar: ui.ProgressBarControl):
- return int(progressBar.GetLegacyIAccessiblePattern().Value)
-
- @staticmethod
- def EditControl(parent: ui.WindowControl, ClassName=None, Name=None, AutomationId=None, timeout=3):
- searchProperties = {}
- if ClassName is not None:
- searchProperties.update({"ClassName": ClassName})
- if Name is not None:
- searchProperties.update({"Name": Name})
- if AutomationId is not None:
- searchProperties.update({"AutomationId": AutomationId})
- edit = parent.EditControl(**searchProperties)
- # 可以判断编辑框是否存在
- if edit.Exists(timeout):
- return edit
- return None
-
- @staticmethod
- def inputEditControlText(editControl: ui.EditControl, text):
- return editControl.GetValuePattern().SetValue(text)
-
- if __name__ == '__main__':
- app = r'D:\程序安装\Notepad++\notepad++.exe'
- AiUiAutomation.setLogPath(r'D:\report')
- sta, window = AiUiAutomation.openApplication(app, Name=None, ClassName="Notepad++")
- if sta:
- AiUiAutomation.clickMenuItemByName(window, Name="设置(T)")
- AiUiAutomation.clickMenuItemBySubName(window, SubName="首选项...", useLoc=True)
- firstChoiceWindow = AiUiAutomation.WindowControl("首选项")
- button = AiUiAutomation.ButtonControl(firstChoiceWindow, Name="关闭")
- AiUiAutomation.clickButton(button)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。