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

# -*- 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)

你可能感兴趣的:(PC软件自动化测试,windows,ui)