Python庫 — это open source проект по автоматизации десктопных GUI приложений на Windows. За последние два года в ней появились новые крупные фичи:
- Поддержка технологии MS UI Automation. Интерфейс прежний, и теперь поддерживаются: WinForms, WPF, Qt5, Windows Store (UWP) и так далее — почти все, что есть на Windows.
- 後端/插件系統(目前有兩個:預設
"win32"和新的"uia")。 然後我們就順利的邁向跨平台了。 - 用於滑鼠和鍵盤的 Win32 掛鉤(本著 pyHook 精神的熱鍵)。
我們也將簡要概述桌面自動化的開源內容(不假裝是認真的比較)。
本文是明斯克 SQA 第 20 天會議報告的部分文字記錄 ( и ),部分俄文版本 對於 pywinauto.
- 基本方法
- 基本桌面輔助技術
讓我們先簡要概述一下該領域的開源。 對於桌面 GUI 應用程式來說,一切都比具有 Selenium 的 Web 應用程式更複雜。 以下是主要方法:
座標法
對點擊點進行硬編碼,我們希望成功點擊。
[+] 跨平台,易於實施。
[+] 可以輕鬆地「錄製-重播」測試錄音。
[-] 對改變螢幕解析度、主題、字體、視窗大小等最不穩定。
[-] 需要大量的支援工作;從頭開始重新產生測試或手動測試通常更容易。
[-] 僅自動執行操作;還有其他方法用於驗證和檢索資料。
工具(跨平台): , , 以及許多其他人。 通常,更複雜的工具包括此功能(並不總是跨平台)。
值得一提的是,座標方法可以補充其他方法。 例如,對於自訂圖形,您可以單擊相對座標(從視窗/元素的左上角,而不是整個螢幕) - 這通常非常可靠,特別是如果您考慮到圖形的長度/寬度整個元素(那麼不同的螢幕解析度不會造成影響)。
另一種選擇:僅分配一台設定穩定的機器進行測試(不能跨平台,但在某些情況下很好)。
參考影像識別
[+] 跨平台[+-] 相對可靠(比座標法好),但仍需要一些技巧。
[-+] 相對較慢,因為識別演算法需要 CPU 資源。
[-] 文字辨識(OCR)通常是不可能的 => 無法取得文字資料。 據我所知,現有的 OCR 解決方案對於此類任務來說不是很可靠,並且沒有廣泛使用(如果還沒有這種情況,歡迎評論)。
儀器儀表: , (Sikuli相容,純Python), .
無障礙技術
[+] 最可靠的方法,因為允許您按文字搜索,無論系統或框架如何呈現它。[+] 允許您提取文字資料 => 更容易驗證測試結果。
[+] 一般來說,最快,因為幾乎不消耗CPU資源。
[-] Тяжело сделать кросс-платформенный инструмент: абсолютно все open-source библиотеки поддерживают одну-две accessibility технологии. Windows/Linux/MacOS целиком не поддерживает никто, кроме платных типа TestComplete, UFT или Squish.
[-] 這種技術原則上並不總是可行。 例如,在 VirtualBox 內測試載入畫面 - 如果沒有影像識別,則無法完成此操作。 但在許多經典案例中,可訪問性方法仍然適用。 這將進一步討論。
儀器儀表: 在 C# 中, 在 C# 中(與 Selenium 相容), 在 C# 中(Appium 相容), , (相容LDTP) , 在紅寶石中, (Linux Desktop Testing Project) и его Windows 版本 .
LDTP 也許是唯一基於可訪問性技術的跨平台開源工具(更準確地說,是一系列函式庫)。 然而,它並不是很受歡迎。 我自己沒有使用過,但根據評論,它的介面並不是最方便的。 如果您有正面的回饋,請在評論中分享。
測試後門(又稱室內自行車)
對於跨平台應用程序,開發人員自己通常會創建內部機制來確保可測試性。 例如,他們在應用程式中建立一個服務 TCP 伺服器,測試連接到它並發送文字命令:點擊什麼、從哪裡獲取資料等。 可靠,但不通用。
基本桌面輔助技術
很好的舊 Win32 API
最 Windows приложений, написанных до выхода WPF и затем Windows Store, построены так или иначе на Win32 API. А именно, MFC, WTL, C++ Builder, Delphi, VB6 — все эти инструменты используют Win32 API. Даже Windows Forms — в значительной степени Win32 API совместимые.
儀器儀表: (類似VB)和Python包裝器 , (自己的語言,有一個IDispatch COM介面), (Python) (紅寶石) (紅寶石)。
微軟使用者介面自動化
Главный плюс: технология MS UI Automation поддерживает подавляющее большинство GUI приложений на Windows за редкими исключениями. Проблема: она не сильно легче в изучении, чем Win32 API. Иначе никто бы не делал оберток над ней.
其實這是一套自訂的COM介面(主要是 UIAutomationCore.dll),並且還有一個 .NET 包裝器,格式為 namespace System.Windows.Automation。 順便說一句,它引入了一個錯誤,因此可能會失去一些 UI 元素。 因此,最好直接使用 UIAutomationCore.dll(如果您聽說過 C# 中的 UiaComWrapper,那麼就是這個)。
COM介面的類型:
(1)基本IUknown-「萬惡之源」。 最低級別,絕不用戶友好。
(2) IDispatch及其衍生性商品(例如 Excel.Application),可以使用 win32com.client 套件(包含在 pyWin32 中)在 Python 中使用。 最方便、最美觀的選擇。
(3) 第三方Python套件可以使用的自訂接口 .
儀器儀表: 在 C# 中, 0.6.0+, 在 C# 中, (UIAutomationCore.dll 上的 C 包裝器的原始碼未公開), 在紅寶石中。
AT-SPI
Несмотря на то, что почти все оси семейства Linux построены на X Window System (в Fedora 25 «иксы» поменяли на Wayland), «иксы» позволяют оперировать только окнами верхнего уровня и мышью/клавиатурой. Для детального разбора по кнопкам, лист боксам и так далее — существует технология AT-SPI. У самых популярных оконных менеджеров есть так называемый AT-SPI registry демон, который и обеспечивает для приложений автоматизируемый GUI (как минимум поддерживаются Qt и GTK).
儀器儀表: .
在我看來,pyatspi2 包含太多像 PyGObject 這樣的依賴項。 該技術本身可作為常規動態庫使用 libatspi.so。 有一個 . Для библиотеки pywinauto планируем реализовать поддержку AT-SPI имеено так: через загрузку libatspi.so и модуль ctypes. Есть небольшая проблема только в использовании нужной версии, ведь для GTK+ и Qt приложений они немного разные. Вероятный выпуск pywinauto 0.7.0 с полноценной поддержкой Linux можно ожидать в первой половине 2018-го.
蘋果輔助使用 API
MacOS 有自己的自動化語言 AppleScript。 當然,要在 Python 中實現這樣的功能,您需要使用 ObjectiveC 中的函數。 從 MacOS 10.6 開始,pyobjc 套件似乎包含在預先安裝的 Python 中。 這也將使列出 pywinauto 中未來支援的依賴項變得更加容易。
工具:除了Apple Script語言之外,還值得關注 ,又名 pyatom。 它與 LDTP 介面相容,但也是一個獨立的庫。 它有 ,是我的學生寫的。 有一個已知的問題:靈活的計時不起作用(方法 waitFor*)。 但總體而言,並不是壞事。
如何開始使用 pywinauto
第一步是使用 GUI 物件檢視器(稱為 Spy 工具)武裝自己。 它將幫助您從內部研究應用程式:元素的層次結構是如何建構的,有哪些屬性可用。 最著名的現場檢查員:
- 間諜++ - 包含在 Visual Studio 中,包括 Express 或 Community Edition。 使用 Win32 API。 他的克隆人也廣為人知 AutoIt 視窗資訊.
- 檢查程式 — входит в Windows SDK. Если он у вас установлен, то на 64-битной Windows можно найти его в папке
C:Program Files (x86)Windows Kits<winver>binx64。 在檢查器本身中,您需要選擇一種模式 用戶界面自動化 而不是 MS AA(Active Accessibility,UI 自動化的祖先)。
徹底檢查應用程式後,我們選擇將使用的後端。 建立Application物件時指定後端的名稱就足夠了。
- 後端=“win32” — 預設使用時,可與 MFC、WTL、VB6 和其他舊版應用程式良好配合。
- 後端=“uia” — новый бэкенд для MS UI Automation: идеально работает с WPF и WinForms; также хорош для Delphi и Windows Store приложений; работает с Qt5 и некоторыми Java приложениями. И вообще, если Inspect.exe видит элементы и их свойства, значит этот бэкенд подходит. В принципе, большинство браузеров тоже поддерживает UI Automation (Mozilla по умолчанию, а Хрому при запуске нужно скормить ключ командной строки
--force-renderer-accessibility查看 Inspect.exe 中頁面上的元素)。 當然,在這個領域與 Selenium 競爭幾乎是不可能的。 只是使用瀏覽器的另一種方式(可能對跨產品場景有用)。
自動化的切入點
該應用已被廣泛研究。 是時候創建一個應用程式物件並運行它或附加到已經運行的應用程式物件。 這不僅僅是標準類的克隆 subprocess.Popen,即一個輸入對象,它將您的所有操作限制在流程的邊界內。 如果應用程式的多個實例正在運行,但您不想觸及其餘實例,則這非常有用。
from pywinauto.application import Application
app = Application(backend="uia").start('notepad.exe')
# Опишем окно, которое хотим найти в процессе Notepad.exe
dlg_spec = app.UntitledNotepad
# ждем пока окно реально появится
actionable_dlg = dlg_spec.wait('visible')如果您想同時管理多個應用程序,此類將幫助您 Desktop。 例如,在Win10上的計算器中,元素的層次結構分佈在多個進程中(不僅 calc.exe)。 所以沒有對象 Desktop 不夠。
from subprocess import Popen
from pywinauto import Desktop
Popen('calc.exe', shell=True)
dlg = Desktop(backend="uia").Calculator
dlg.wait('visible')根對象(Application 或 Desktop) 是唯一需要指定後端的地方。 其他所有內容都明顯屬於“規範->包裝器”概念,稍後將對此進行討論。
視窗/元件規格
這是建立 pywinauto 介面的核心概念。 您可以粗略或更詳細地描述視窗/元素,即使它尚不存在或已關閉。 視窗規格(對象 窗戶規格) 儲存搜尋真實視窗或元素的標準。
詳細視窗規格範例:
>>> dlg_spec = app.window(title='Untitled - Notepad')
>>> dlg_spec
<pywinauto.application.WindowSpecification object at 0x0568B790>
>>> dlg_spec.wrapper_object()
<pywinauto.controls.win32_controls.DialogWrapper object at 0x05639B70>視窗搜尋本身是透過呼叫該方法進行的 .wrapper_object()。 它會傳回真實視窗/元素的某個「包裝器」或拋出異常 ElementNotFoundError (有時 ElementAmbiguousError,如果找到多個元素,即需要明確搜尋條件)。 這個「包裝器」已經知道如何對元素執行某些操作或從中接收資料。
Python可以隱藏調用 .wrapper_object(),所以最終的程式碼變得更短。 我們建議僅將其用於調試目的。 接下來的兩行做了完全相同的事:
dlg_spec.wrapper_object().minimize() # debugging
dlg_spec.minimize() # production窗戶規格有許多搜尋標準。 這裡僅舉幾個例子:
# могут иметь несколько уровней
app.window(title_re='.* - Notepad$').window(class_name='Edit')
# можно комбинировать критерии (как AND) и не ограничиваться одним процессом приложения
dlg = Desktop(backend="uia").Calculator
dlg.window(auto_id='num8Button', control_type='Button')所有可能標準的清單位於函數文件中 .
透過屬性和鍵存取的魔力
Python 可以輕鬆建立視窗規範並動態識別物件屬性(在內部,該方法被重寫 __getattribute__)。 當然,屬性名稱受到與任何變數名稱相同的限制(不能插入空格、逗號或其他特殊字元)。 幸運的是,pywinauto 使用所謂的「最佳匹配」搜尋演算法,可以防止拼字錯誤和小變化。
app.UntitledNotepad
# то же самое, что
app.window(best_match='UntitledNotepad')如果您仍然需要 Unicode 字串(例如,俄語)、空格等,您可以透過鍵存取(就像它是普通字典一樣):
app['Untitled - Notepad']
# то же самое, что
app.window(best_match='Untitled - Notepad')神奇名字的五個規則
如何找出標準的魔法名稱? 在搜尋之前分配給元素的那些。 如果您指定的名稱與標準足夠相似,則將找到該元素。
- 按標題(文字、名稱):
app.Properties.OK.click() - 按文字和元素類型:
app.Properties.OKButton.click() - 按類型和數量:
app.Properties.Button3.click()(名字Button0иButton1綁定到找到的第一個元素,Button2- 到第二個,然後按順序 - 這就是歷史上發生的情況) - 按靜態文字(左側或頂部)和類型:
app.OpenDialog.FileNameEdit.set_text("")(對於具有動態文字的元素很有用) - 按類型和內部文字:
app.Properties.TabControlSharing.select("General")
通常同時應用兩到三個規則,很少會更多。 要檢查每個元素有哪些特定名稱,可以使用下列方法 print_control_identifiers()。 它可以將元素樹列印到螢幕和檔案中。 對於每個元素,都會列印其標準魔法名稱。 您還可以從那裡複製並貼上更詳細的子元素規範。 腳本中的結果將如下所示:
app.Properties.child_window(data-gt-translate-attributes='["title"]' title="Contains:", auto_id="13087", control_type="Edit")元素樹本身通常是一塊相當大的腳布。
>>> app.Properties.print_control_identifiers()
Control Identifiers:
Dialog - 'Windows NT Properties' (L688, T518, R1065, B1006)
[u'Windows NT PropertiesDialog', u'Dialog', u'Windows NT Properties']
child_window(data-gt-translate-attributes='["title"]' title="Windows NT Properties", control_type="Window")
|
| Image - '' (L717, T589, R749, B622)
| [u'', u'0', u'Image1', u'Image0', 'Image', u'1']
| child_window(auto_id="13057", control_type="Image")
|
| Image - '' (L717, T630, R1035, B632)
| ['Image2', u'2']
| child_window(auto_id="13095", control_type="Image")
|
| Edit - 'Folder name:' (L790, T596, R1036, B619)
| [u'3', 'Edit', u'Edit1', u'Edit0']
| child_window(data-gt-translate-attributes='["title"]' title="Folder name:", auto_id="13156", control_type="Edit")
|
| Static - 'Type:' (L717, T643, R780, B658)
| [u'Type:Static', u'Static', u'Static1', u'Static0', u'Type:']
| child_window(data-gt-translate-attributes='["title"]' title="Type:", auto_id="13080", control_type="Text")
|
| Edit - 'Type:' (L790, T643, R1036, B666)
| [u'4', 'Edit2', u'Type:Edit']
| child_window(data-gt-translate-attributes='["title"]' title="Type:", auto_id="13059", control_type="Edit")
|
| Static - 'Location:' (L717, T669, R780, B684)
| [u'Location:Static', u'Location:', u'Static2']
| child_window(data-gt-translate-attributes='["title"]' title="Location:", auto_id="13089", control_type="Text")
|
| Edit - 'Location:' (L790, T669, R1036, B692)
| ['Edit3', u'Location:Edit', u'5']
| child_window(data-gt-translate-attributes='["title"]' title="Location:", auto_id="13065", control_type="Edit")
|
| Static - 'Size:' (L717, T695, R780, B710)
| [u'Size:Static', u'Size:', u'Static3']
| child_window(data-gt-translate-attributes='["title"]' title="Size:", auto_id="13081", control_type="Text")
|
| Edit - 'Size:' (L790, T695, R1036, B718)
| ['Edit4', u'6', u'Size:Edit']
| child_window(data-gt-translate-attributes='["title"]' title="Size:", auto_id="13064", control_type="Edit")
|
| Static - 'Size on disk:' (L717, T721, R780, B736)
| [u'Size on disk:', u'Size on disk:Static', u'Static4']
| child_window(data-gt-translate-attributes='["title"]' title="Size on disk:", auto_id="13107", control_type="Text")
|
| Edit - 'Size on disk:' (L790, T721, R1036, B744)
| ['Edit5', u'7', u'Size on disk:Edit']
| child_window(data-gt-translate-attributes='["title"]' title="Size on disk:", auto_id="13106", control_type="Edit")
|
| Static - 'Contains:' (L717, T747, R780, B762)
| [u'Contains:1', u'Contains:0', u'Contains:Static', u'Static5', u'Contains:']
| child_window(data-gt-translate-attributes='["title"]' title="Contains:", auto_id="13088", control_type="Text")
|
| Edit - 'Contains:' (L790, T747, R1036, B770)
| [u'8', 'Edit6', u'Contains:Edit']
| child_window(data-gt-translate-attributes='["title"]' title="Contains:", auto_id="13087", control_type="Edit")
|
| Image - 'Contains:' (L717, T773, R1035, B775)
| [u'Contains:Image', 'Image3', u'Contains:2']
| child_window(data-gt-translate-attributes='["title"]' title="Contains:", auto_id="13096", control_type="Image")
|
| Static - 'Created:' (L717, T786, R780, B801)
| [u'Created:', u'Created:Static', u'Static6', u'Created:1', u'Created:0']
| child_window(data-gt-translate-attributes='["title"]' title="Created:", auto_id="13092", control_type="Text")
|
| Edit - 'Created:' (L790, T786, R1036, B809)
| [u'Created:Edit', 'Edit7', u'9']
| child_window(data-gt-translate-attributes='["title"]' title="Created:", auto_id="13072", control_type="Edit")
|
| Image - 'Created:' (L717, T812, R1035, B814)
| [u'Created:Image', 'Image4', u'Created:2']
| child_window(data-gt-translate-attributes='["title"]' title="Created:", auto_id="13097", control_type="Image")
|
| Static - 'Attributes:' (L717, T825, R780, B840)
| [u'Attributes:Static', u'Static7', u'Attributes:']
| child_window(data-gt-translate-attributes='["title"]' title="Attributes:", auto_id="13091", control_type="Text")
|
| CheckBox - 'Read-only (Only applies to files in folder)' (L790, T825, R1035, B841)
| [u'CheckBox0', u'CheckBox1', 'CheckBox', u'Read-only (Only applies to files in folder)CheckBox', u'Read-only (Only applies to files in folder)']
| child_window(data-gt-translate-attributes='["title"]' title="Read-only (Only applies to files in folder)", auto_id="13075", control_type="CheckBox")
|
| CheckBox - 'Hidden' (L790, T848, R865, B864)
| ['CheckBox2', u'HiddenCheckBox', u'Hidden']
| child_window(data-gt-translate-attributes='["title"]' title="Hidden", auto_id="13076", control_type="CheckBox")
|
| Button - 'Advanced...' (L930, T845, R1035, B868)
| [u'Advanced...', u'Advanced...Button', 'Button', u'Button1', u'Button0']
| child_window(data-gt-translate-attributes='["title"]' title="Advanced...", auto_id="13154", control_type="Button")
|
| Button - 'OK' (L814, T968, R889, B991)
| ['Button2', u'OK', u'OKButton']
| child_window(data-gt-translate-attributes='["title"]' title="OK", auto_id="1", control_type="Button")
|
| Button - 'Cancel' (L895, T968, R970, B991)
| ['Button3', u'CancelButton', u'Cancel']
| child_window(data-gt-translate-attributes='["title"]' title="Cancel", auto_id="2", control_type="Button")
|
| Button - 'Apply' (L976, T968, R1051, B991)
| ['Button4', u'ApplyButton', u'Apply']
| child_window(data-gt-translate-attributes='["title"]' title="Apply", auto_id="12321", control_type="Button")
|
| TabControl - '' (L702, T556, R1051, B962)
| [u'10', u'TabControlSharing', u'TabControlPrevious Versions', u'TabControlSecurity', u'TabControl', u'TabControlCustomize']
| child_window(auto_id="12320", control_type="Tab")
| |
| | TabItem - 'General' (L704, T558, R753, B576)
| | [u'GeneralTabItem', 'TabItem', u'General', u'TabItem0', u'TabItem1']
| | child_window(data-gt-translate-attributes='["title"]' title="General", control_type="TabItem")
| |
| | TabItem - 'Sharing' (L753, T558, R801, B576)
| | [u'Sharing', u'SharingTabItem', 'TabItem2']
| | child_window(data-gt-translate-attributes='["title"]' title="Sharing", control_type="TabItem")
| |
| | TabItem - 'Security' (L801, T558, R851, B576)
| | [u'Security', 'TabItem3', u'SecurityTabItem']
| | child_window(data-gt-translate-attributes='["title"]' title="Security", control_type="TabItem")
| |
| | TabItem - 'Previous Versions' (L851, T558, R947, B576)
| | [u'Previous VersionsTabItem', u'Previous Versions', 'TabItem4']
| | child_window(data-gt-translate-attributes='["title"]' title="Previous Versions", control_type="TabItem")
| |
| | TabItem - 'Customize' (L947, T558, R1007, B576)
| | [u'CustomizeTabItem', 'TabItem5', u'Customize']
| | child_window(data-gt-translate-attributes='["title"]' title="Customize", control_type="TabItem")
|
| TitleBar - 'None' (L712, T521, R1057, B549)
| ['TitleBar', u'11']
| |
| | Menu - 'System' (L696, T526, R718, B548)
| | [u'System0', u'System', u'System1', u'Menu', u'SystemMenu']
| | child_window(data-gt-translate-attributes='["title"]' title="System", auto_id="MenuBar", control_type="MenuBar")
| | |
| | | MenuItem - 'System' (L696, T526, R718, B548)
| | | [u'System2', u'MenuItem', u'SystemMenuItem']
| | | child_window(data-gt-translate-attributes='["title"]' title="System", control_type="MenuItem")
| |
| | Button - 'Close' (L1024, T519, R1058, B549)
| | [u'CloseButton', u'Close', 'Button5']
| | child_window(data-gt-translate-attributes='["title"]' title="Close", control_type="Button")在某些情況下,列印整個樹可能會很慢(例如,在 iTunes 中,一個選項卡上有多達三千個元素!),但您可以使用該選項 depth (深度): depth=1 - 元素本身, depth=2 - 僅限直系子女,依此類推。 也可以在建立時在規格中指定 child_window.
Примеры
我們正在不斷補充 。 在最近的這些中,值得注意的是 WireShark 網路分析儀的自動化(這是 Qt5 應用程式的一個很好的例子;雖然這個任務可以在沒有 GUI 的情況下解決,因為有 scapy.Sniffer 來自Python包 )。 還有一個帶有功能區工具列的 MS Paint 自動化範例。
我的一個學生寫的另一個很好的例子: (稍後它將移至主存儲庫)。
當然,還有一個訂閱鍵盤(熱鍵)和滑鼠事件的範例:
.
致謝
特別感謝那些不斷幫助開發該專案的人。 對於我和 這是一個永久的愛好。 我的兩位來自新諾伊大學的學生最近就這個主題捍衛了他們的學士學位。 為支援 MS UI 自動化做出了巨大貢獻,最近開始製作一個基於文字屬性(這是最複雜的功能)的「記錄播放」原理的自動程式碼產生器,到目前為止僅適用於「uia」後端。 разрабатывает новый бэкенд под Linux на основе AT-SPI (модули mouse и keyboard 基於 - 已在版本 0.6.x 中)。
由於我已經用 Python 教授一門關於自動化的特殊課程相當長一段時間了,一些碩士生會做作業,實現自動化的小功能或範例。 研究階段的一些關鍵東西也曾經被學生挖掘出來。 儘管有時你必須嚴格監控程式碼的品質。 程式碼覆蓋率約 95% 的靜態分析器(QuantifiedCode、Codacy 和 Landscape)和雲端自動測試(AppVeyor 服務)對此有很大幫助。
也要感謝所有留下評論、提出錯誤和發送拉取請求的人!
其他資源
我們跟進問題 (最近出現 )和 。 有 .
我們每個月都會更新 。 就GitHub 上的星星數量而言,只有Autohotkey(他們擁有非常大的社區和悠久的歷史)和PyAutoGUI 增長得更快(很大程度上歸功於其作者Al Sweigart 的書籍的受歡迎程度:“Automate the Boring Stuff with Python」等)。
來源: www.habr.com
