用 Python 写的自动化测试 WPF 程序的一个案例
背景
在日常开发和测试工作中,我们需要对 WPF 桌面应用程序进行功能验证。传统的手工点击方式效率低、容易出错,特别是一些需要重复执行的操作,人工测试不仅耗时,而且难以保证一致性。
因此,我尝试用 Python 来实现自动化测试,通过脚本来启动和操作 WPF 程序,完成一些表单填写和操作的验证。
遇到的问题
在实现过程中,主要遇到几个问题:
进程连接困难
一开始通过psutil
找到目标进程 PID 没问题,但在用pywinauto
连接时,总是提示Process with PID=xxxx not found
。
原因是目标进程虽然启动了,但窗口还没完全初始化,或者运行权限不一致。控件识别不稳定
WPF 程序的控件有时用uia
后端才能识别,有时需要win32
。一开始只用单一后端会导致找不到控件。执行速度与同步问题
自动化脚本运行太快,程序界面还没加载完就开始操作,导致控件查找失败
解决思路
针对以上问题,我采取了以下思路:
封装进程连接方法
写了get_app_window
方法,先用psutil
找到目标进程,再用pywinauto.Application.connect
连接窗口,并增加了backend
(默认为uia
)和wait_time
(默认为 5 秒)的参数,确保能灵活切换后端并等待窗口加载完成。默认值和重试机制
在方法内部设置默认参数,避免每次调用都要传。并预留了超时和重试机制,保证在窗口加载慢时也能成功。封装操作逻辑
对常用操作(如填写表单)进行了函数封装,比如fill_subject_form()
,这样测试用例更清晰,后续要换别的数据也很方便。
具体方案
代码如下:
import psutil
import time
import os
from pywinauto import Application
from pywinauto.mouse import clickdef get_app_window(process_name: str, backend: str = "uia", wait_time: float = 5.0):pid = Nonefor proc in psutil.process_iter(['pid', 'name']):if proc.info['name'] and proc.info['name'].lower() == process_name.lower():pid = proc.info['pid']breakif not pid:raise RuntimeError(f"未找到进程:{process_name}")# 直接用默认 backend 和 wait_timeapp = Application(backend=backend).connect(process=pid, timeout=wait_time)win = app.top_window()return windef generate_incremental_num(counter_file="counter.txt"):"""递增编号生成器,返回 3 位字符串编号"""if not os.path.exists(counter_file):with open(counter_file, "w") as f:f.write("1")with open(counter_file, "r") as f:num = int(f.read().strip())new_num = num + 1with open(counter_file, "w") as f:f.write(str(new_num))return f"{num:03d}" # 格式化为 '001'def select_position(win, main_area, sub_area=None):"""点击主部位和子部位按钮"""try:win.child_window(title=main_area, control_type="Button").click_input()print(f"✅ 已点击主部位:{main_area}")time.sleep(0.5)if sub_area:win.child_window(title=sub_area, control_type="Button").click_input()print(f"✅ 已点击子部位:{sub_area}")except Exception as e:print(f"❌ 点击部位失败:{e}")def wait_for_checkboxes(win, timeout=5):"""等待复选框出现"""for _ in range(timeout * 2): # 每0.5秒查一次checkboxes = win.descendants(control_type="CheckBox")if len(checkboxes) > 1:return checkboxestime.sleep(0.5)raise RuntimeError("超时:复选框仍未出现")def click_first_row_of_patient_grid(win):"""模拟点击 patientGrid 第一行(通过坐标)"""try:grid = win.child_window(auto_id="patientGrid", control_type="DataGrid")rect = grid.rectangle()# 假设表头约 40 像素,点击第 1 行中间位置x = rect.left + 20y = rect.top + 50 # 跳过表头高度from pywinauto.mouse import clickclick(button='left', coords=(x, y))print(f"✅ 已模拟点击 patientGrid 第一行坐标 ({x}, {y})")time.sleep(0.5)except Exception as e:import tracebackprint(f"❌ 点击 patientGrid 第一行失败:{e}")traceback.print_exc()def click_detect_button(win):try:btnExamination = win.child_window(auto_id="btnExamination", control_type="Button")btnExamination.click_input()print("✅ 已点击“检测”按钮")except Exception as e:print(f"❌ 点击“检测”失败:{e}")def perform_detection_action(win):"""在检测界面依次点击 btnLive 和 btnSnap"""try:print("🎬 开始检测页面点击操作...")# 点击 btnLivefor i in range(10000):print(f"\n🔁 第 {i + 1}/10000 次执行...")btn_live = win.child_window(auto_id="btnLive", control_type="Button")btn_live.click_input()print("✅ 已点击 btnLive")time.sleep(10) # 等待 8 秒# 点击 btnSnapbtn_snap = win.child_window(auto_id="btnSnap", control_type="Button")btn_snap.click_input()print("✅ 已点击 btnSnap")time.sleep(8)# 点击 btnZstackFastbtn_zstack = win.child_window(auto_id="btnZstackFast", control_type="Button")btn_zstack.click_input()print("✅ 已点击 btnZstackFast")time.sleep(60)except Exception as e:print(f"❌ 检测页面按钮点击失败:{e}")def fill_subject_form(win, name, num, sex, age, height, weight, project, main_area, sub_area):print("⏳ 正在填写表单...")# 点击“新增”try:btnAdd = win.child_window(auto_id="btnAdd", control_type="Button")btnAdd.click_input()print("✅ 已点击“新增”按钮")time.sleep(1.0)except:raise RuntimeError("❌ 找不到“新增”按钮")# 获取 Edit 控件组edits = win.descendants(control_type="Edit")# 姓名win.child_window(auto_id="_SubjectNameTextBox_", control_type="Edit").set_edit_text(name)# 编号edits[1].set_edit_text(num)# 年龄、身高、体重edits[2].set_edit_text(str(age))edits[3].set_edit_text(str(height))edits[4].set_edit_text(str(weight))# 受试部位(文本框 + 按钮点击)win.child_window(auto_id="PositionResult", control_type="Edit").set_edit_text(sub_area)select_position(win, main_area, sub_area)# 点击“确认”btnOk = win.child_window(auto_id="btnOk", control_type="Button")btnOk.click_input()print(f"✅ 编号 {num} 提交完成")time.sleep(2)win = get_app_window("780.exe")# 等复选框加载完成,再点击“检测”click_first_row_of_patient_grid(win)click_detect_button(win)time.sleep(1.5)# 再次获取当前窗口(检测界面可能为新窗口)detect_win = get_app_window("780.exe")perform_detection_action(detect_win)if __name__ == "__main__":try:win = get_app_window("780.exe")# 获取自动编号num = generate_incremental_num()# 调用填写函数fill_subject_form(win=win,name="张三",num=num,sex="Male",age=30,height=175,weight=70,project="111",main_area="头颈部",sub_area="左面颊")except Exception as e:print(f"❌ 自动化失败:{e}")
总结
通过这个小案例,我基本跑通了 Python + psutil + pywinauto 的组合,解决了“如何定位 WPF 窗口并自动化操作”的问题。