Windows 桌面 E2E 测试¶
pywinauto 和 Windows UI Automation(UIA)用于 Windows 原生桌面应用程序的端到端测试。覆盖 WPF、WinForms、Win32/MFC、Qt(5.x / 6.x),Qt 特定指南作为专用章节提供。
何时激活¶
- 编写或运行 Windows 原生桌面应用程序的端到端测试时
- 从零开始设置桌面 GUI 测试套件时
- 诊断不稳定或失败的桌面自动化测试时
- 向现有应用添加可测试性(AutomationId、可访问名称)时
- 将桌面端到端测试集成到 CI/CD 流水线(GitHub Actions
windows-latest)时
何时不使用¶
- Web 应用 → 使用
e2e-testing技能(Playwright) - Electron / CEF / WebView2 应用 → HTML 层需要浏览器自动化,而不是 UIA
- 移动应用 → 使用平台特定工具(UIAutomator、XCUITest)
- 不需要运行 GUI 的纯单元测试或集成测试
核心概念¶
所有 Windows 桌面自动化都依赖 UI Automation(UIA)。这是 Windows 内置的辅助功能 API。所有受支持的框架都会公开一个 UIA 元素树,这些元素具有可读取和可操作的属性:
テスト(Python)
└── pywinauto(UIAバックエンド)
└── Windows UI Automation API ← Windowsに組み込み、フレームワーク非依存
└── アプリのUIAプロバイダー ← 各フレームワークが独自に実装
└── 実行中の .exe
各框架 UIA 质量:
| 框架 | AutomationId | 可靠性 | 备注 |
|---|---|---|---|
| WPF | 5/5 | 优秀 | x:Name 直接映射到 AutomationId |
| WinForms | 4/5 | 良好 | AccessibleName = AutomationId |
| UWP / WinUI 3 | 5/5 | 优秀 | Microsoft 完全支持 |
| Qt 6.x | 5/5 | 优秀 | 辅助功能默认启用;类名更改为 Qt6* |
| Qt 5.15+ | 4/5 | 良好 | Accessibility 模块得到改进 |
| Qt 5.7–5.14 | 3/5 | 一般 | 需要 QT_ACCESSIBILITY=1;objectName 需手动设置 |
| Win32 / MFC | 3/5 | 一般 | 可访问控件 ID;文本匹配常见 |
设置与前提条件¶
# Python 3.8+、Windowsのみ
pip install pywinauto pytest pytest-html Pillow pytest-timeout
# オプション:画面録画
# ffmpegをインストールしてPATHに追加:https://ffmpeg.org/download.html
确认 UIA 是否可达:
请安装 Accessibility Insights for Windows(Microsoft 提供,免费)—— 它是用于在编写测试前检查 UIA 元素树的 DevTools 等效工具。
可测试性设置(按框架)¶
在编写测试之前,为所有交互式控件设置稳定的 AutomationId 是最有效的做法。
WPF¶
<!-- XAML: x:Name が自動的にAutomationIdになる -->
<TextBox x:Name="usernameInput" />
<PasswordBox x:Name="passwordInput" />
<Button x:Name="btnLogin" Content="Login" />
<TextBlock x:Name="lblError" />
WinForms¶
// デザイナーまたはコードで設定
usernameInput.AccessibleName = "usernameInput";
passwordInput.AccessibleName = "passwordInput";
btnLogin.AccessibleName = "btnLogin";
lblError.AccessibleName = "lblError";
Win32 / MFC¶
// .rcファイルのコントロールリソースIDがAutomationId文字列として公開される
// IDC_EDIT_USERNAME -> AutomationId "1001"
// 名前にはSetWindowTextを優先;より豊かなサポートにはIAccessibleを追加する
Qt — 参见下方专用章节¶
页面对象模型¶
tests/
├── conftest.py # アプリ起動フィクスチャ、失敗時スクリーンショット
├── pytest.ini
├── config.py
├── pages/
│ ├── __init__.py # インポートに必須
│ ├── base_page.py # ロケーター、ウェイト、スクリーンショットヘルパー
│ ├── login_page.py
│ └── main_page.py
├── tests/
│ ├── __init__.py
│ ├── test_login.py
│ └── test_main_flow.py
└── artifacts/ # スクリーンショット、動画、ログ
base_page.py¶
import os, time
from pywinauto import Desktop
from config import ACTION_TIMEOUT, ARTIFACT_DIR
class BasePage:
def __init__(self, window):
self.window = window
# --- ロケーター(優先順位順)---
def by_id(self, auto_id, **kw):
"""AutomationId — 最も安定。第一選択として使用する。"""
return self.window.child_window(auto_id=auto_id, **kw)
def by_name(self, name, **kw):
"""表示テキスト / アクセシブルな名前。"""
return self.window.child_window(title=name, **kw)
def by_class(self, cls, index=0, **kw):
"""コントロールクラス + インデックス — 脆弱、可能なら避ける。"""
return self.window.child_window(class_name=cls, found_index=index, **kw)
# --- ウェイト ---
def wait_visible(self, spec, timeout=ACTION_TIMEOUT):
spec.wait("visible", timeout=timeout)
return spec
def wait_gone(self, spec, timeout=ACTION_TIMEOUT):
spec.wait_not("visible", timeout=timeout)
return spec
def wait_window(self, title, timeout=ACTION_TIMEOUT):
"""新しいトップレベルウィンドウ(ダイアログ、子ウィンドウ)を待つ。"""
dlg = Desktop(backend="uia").window(title=title)
dlg.wait("visible", timeout=timeout)
return dlg
def wait_until(self, fn, timeout=ACTION_TIMEOUT, interval=0.3):
"""任意の条件をポーリング — UIAイベントが信頼できない場合に使用する。"""
deadline = time.time() + timeout
while time.time() < deadline:
try:
if fn():
return True
except Exception:
pass
time.sleep(interval)
raise TimeoutError(f"条件が{timeout}秒以内に満たされなかった")
# --- アクション ---
def click(self, spec):
self.wait_visible(spec)
spec.click_input()
def type_text(self, spec, text):
self.wait_visible(spec)
ctrl = spec.wrapper_object()
try:
ctrl.set_edit_text(text)
except Exception as e:
# Qt 5.x フォールバック:UIA Value Pattern が不完全な場合がある
import sys, pywinauto.keyboard as kb
print(f"[windows-desktop-e2e] set_edit_text 失敗 ({e})、キーボードフォールバックを使用", file=sys.stderr)
ctrl.click_input()
kb.send_keys("^a")
kb.send_keys(text, with_spaces=True)
def get_text(self, spec):
ctrl = spec.wrapper_object()
for attr in ("window_text", "get_value"):
try:
v = getattr(ctrl, attr)()
if v:
return v
except Exception:
pass
return ""
# --- アーティファクト ---
def screenshot(self, name):
os.makedirs(ARTIFACT_DIR, exist_ok=True)
path = os.path.join(ARTIFACT_DIR, f"{name}.png")
self.window.capture_as_image().save(path)
return path
login_page.py¶
from pages.base_page import BasePage
class LoginPage(BasePage):
@property
def username(self): return self.by_id("usernameInput")
@property
def password(self): return self.by_id("passwordInput")
@property
def btn_login(self): return self.by_id("btnLogin")
@property
def error_label(self): return self.by_id("lblError")
def login(self, user, pwd):
self.type_text(self.username, user)
self.type_text(self.password, pwd)
self.click(self.btn_login)
def login_ok(self, user, pwd, main_title="Main Window"):
self.login(user, pwd)
return self.wait_window(main_title)
def login_fail(self, user, pwd):
self.login(user, pwd)
self.wait_visible(self.error_label)
return self.get_text(self.error_label)
conftest.py¶
在新项目中,请优先使用 Tier 1 沙盒 fixture(参见下文)——它可以在零额外成本的情况下增加文件系统隔离。此基础 fixture 仅用于最小化/遗留设置。
import os, pytest
os.environ["QT_ACCESSIBILITY"] = "1" # Qt 5.x UIAサポートに必要
from pywinauto import Application
from config import APP_PATH, MAIN_WINDOW_TITLE, LAUNCH_TIMEOUT, ARTIFACT_DIR
@pytest.fixture
def app(request):
if not APP_PATH:
pytest.exit("APP_PATH 環境変数が設定されていない", returncode=1)
proc = Application(backend="uia").start(APP_PATH, timeout=LAUNCH_TIMEOUT)
win = proc.window(title=MAIN_WINDOW_TITLE)
win.wait("visible", timeout=LAUNCH_TIMEOUT)
yield win
# 失敗時のスクリーンショット
if getattr(getattr(request.node, "rep_call", None), "failed", False):
os.makedirs(ARTIFACT_DIR, exist_ok=True)
try:
win.capture_as_image().save(
os.path.join(ARTIFACT_DIR, f"FAIL_{request.node.name}.png")
)
except Exception:
pass
# グレースフルな終了を試み、フォールバックとして強制終了
# proc は pywinauto Application — wait_for_process() ではなく wait_for_process_exit() を使用
try:
win.close()
proc.wait_for_process_exit(timeout=5)
except Exception:
proc.kill()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result())
config.py¶
import os
APP_PATH = os.environ.get("APP_PATH", "") # 環境変数で設定 — デフォルトパスなし
MAIN_WINDOW_TITLE = os.environ.get("APP_TITLE", "")
LAUNCH_TIMEOUT = int(os.environ.get("LAUNCH_TIMEOUT", "15"))
ACTION_TIMEOUT = int(os.environ.get("ACTION_TIMEOUT", "10"))
ARTIFACT_DIR = os.path.join(os.path.dirname(__file__), "artifacts")
pytest.ini¶
[pytest]
testpaths = tests
markers =
smoke: 重要なパスの高速スモークテスト
flaky: 既知の不安定なテスト
addopts = -v --tb=short --html=artifacts/report.html --self-contained-html
定位器策略¶
在 Accessibility Insights 的 Properties 窗格中检查 → 首先确认 AutomationId。
# 実行時の検査 — REPLに貼り付けてツリーを探索
win.print_control_identifiers()
# またはスコープを絞る:
win.child_window(auto_id="groupBox1").print_control_identifiers()
等待模式¶
# コントロールが表示されるのを待つ
page.wait_visible(page.by_id("statusLabel"))
# コントロールが消えるのを待つ(ローディングスピナーなど)
page.wait_gone(page.by_id("spinnerOverlay"))
# ダイアログが表示されるのを待つ
dlg = page.wait_window("Confirm Delete")
# カスタム条件(テキストの変化など)
page.wait_until(lambda: page.get_text(page.by_id("lblStatus")) == "Ready")
不要将 time.sleep() 作为主要同步手段 — 请使用 wait() 或 wait_until()。
工件管理¶
# オンデマンドスクリーンショット
page.screenshot("after_login")
# フルスクリーンキャプチャ(ウィンドウが画面外または最小化されている場合)
import pyautogui
pyautogui.screenshot("artifacts/fullscreen.png")
# ffmpegによる画面録画(テスト前に開始し、テスト後に停止)
import subprocess
def start_recording(name):
return subprocess.Popen([
"ffmpeg", "-f", "gdigrab", "-framerate", "10",
"-i", "desktop", "-y", f"artifacts/videos/{name}.mp4"
], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def stop_recording(proc):
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
处理不稳定测试¶
# 隔離 — PlaywrightのtestのFixmeと同等
@pytest.mark.skip(reason="不安定:遅いCIでのアニメーションレース。Issue #42")
def test_animated_transition(self, app): ...
# CIのみでスキップ
@pytest.mark.skipif(os.environ.get("CI") == "true", reason="CIで不安定 #43")
def test_heavy_load(self, app): ...
常见原因与修复:
| 原因 | 修复 |
|---|---|
| 控件未就绪 | 将 time.sleep 替换为 wait_visible |
| 窗口未获得焦点 | 在交互前添加 win.set_focus() |
| 动画进行中 | wait_until(lambda: not loading_indicator.exists()) |
| 对话框时序问题 | wait_window(title, timeout=15) |
| CI 环境显示器未就绪 | 设置 DISPLAY 或在 CI 中使用虚拟桌面 |
测试隔离与沙盒¶
隔离的三个层级 — 请使用能满足需求的最轻量层级。
Tier 1 — 文件系统隔离(默认,始终使用)¶
每个测试通过 subprocess.Popen 和 Application.connect() 获得独立的 APPDATA / LOCALAPPDATA / TEMP。pytest 的 tmp_path fixture 会自动处理清理。
# conftest.py — 基本的な `app` フィクスチャをこれに置き換える
import os, subprocess, pytest
from pywinauto import Application
from config import APP_PATH, APP_ARGS, APP_TITLE, LAUNCH_TIMEOUT, ACTION_TIMEOUT, ARTIFACT_DIR
@pytest.fixture(scope="function")
def app(request, tmp_path):
"""テストごとに新しいプロセス + 分離されたユーザーデータディレクトリ。"""
if not APP_PATH:
pytest.exit("APP_PATH が設定されていない", returncode=1)
# 全てのユーザーストレージを分離されたtmpディレクトリにリダイレクト
sandbox_env = os.environ.copy()
sandbox_env["QT_ACCESSIBILITY"] = "1"
sandbox_env["APPDATA"] = str(tmp_path / "AppData" / "Roaming")
sandbox_env["LOCALAPPDATA"] = str(tmp_path / "AppData" / "Local")
sandbox_env["TEMP"] = sandbox_env["TMP"] = str(tmp_path / "Temp")
for p in (sandbox_env["APPDATA"], sandbox_env["LOCALAPPDATA"], sandbox_env["TEMP"]):
os.makedirs(p, exist_ok=True)
if not APP_TITLE:
pytest.exit("APP_TITLE 環境変数が設定されていない", returncode=1)
# shlex.splitはスペースを含む引用符付き引数を処理;plain split()は壊れる
import shlex
# subprocessで起動して環境変数を渡し;PIDでpywinautoを接続
proc = subprocess.Popen(
[APP_PATH] + shlex.split(APP_ARGS),
env=sandbox_env,
)
pw_app = Application(backend="uia").connect(process=proc.pid, timeout=LAUNCH_TIMEOUT)
win = pw_app.window(title=APP_TITLE)
win.wait("visible", timeout=LAUNCH_TIMEOUT)
yield win
if getattr(getattr(request.node, "rep_call", None), "failed", False):
os.makedirs(ARTIFACT_DIR, exist_ok=True)
try:
win.capture_as_image().save(
os.path.join(ARTIFACT_DIR, f"FAIL_{request.node.name}.png")
)
except Exception:
pass
try:
win.close()
proc.wait(timeout=5)
except Exception:
proc.kill()
# tmp_pathはpytestによって自動的にクリーンアップされる
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result())
Tier 2 — Windows 作业对象(可选:进程生命周期隔离)¶
将进程附加到作业对象,以便在测试夹具的作业句柄被 GC 时自动终止它。同时,也能防止子进程逃逸出夹具清理。
隔离范围: 作业对象不会虚拟化文件系统访问, 也不会阻止网络流量。文件写入和网络隔离需要 AppContainer、Windows 防火墙规则,或 Tier 3(Windows 沙盒)。 请仅将 Tier 2 用于进程生命周期和子进程隔离。
无需额外依赖。
import ctypes, ctypes.wintypes as wt
def restrict_process(pid: int):
"""
プロセスをジョブオブジェクトにアタッチして以下を防止:
- ジョブ外でのプロセスのスポーン(LIMIT_KILL_ON_JOB_CLOSE)
ネットワークはブロックしません — Windowsファイアウォールルールを使用してください。
"""
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
# 最小限の権限:SET_QUOTA (0x0100) | TERMINATE (0x0001)
PROCESS_SET_QUOTA_AND_TERMINATE = 0x0101
kernel32 = ctypes.windll.kernel32
job = kernel32.CreateJobObjectW(None, None)
hproc = kernel32.OpenProcess(PROCESS_SET_QUOTA_AND_TERMINATE, False, pid)
# 正しい構造体レイアウト — LimitFlagsはオフセット+16(+44ではない)
class JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
_fields_ = [
("PerProcessUserTimeLimit", wt.LARGE_INTEGER),
("PerJobUserTimeLimit", wt.LARGE_INTEGER),
("LimitFlags", wt.DWORD),
("MinimumWorkingSetSize", ctypes.c_size_t),
("MaximumWorkingSetSize", ctypes.c_size_t),
("ActiveProcessLimit", wt.DWORD),
("Affinity", ctypes.c_size_t),
("PriorityClass", wt.DWORD),
("SchedulingClass", wt.DWORD),
]
info = JOBOBJECT_BASIC_LIMIT_INFORMATION()
info.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
ok = kernel32.SetInformationJobObject(job, 2, ctypes.byref(info), ctypes.sizeof(info))
if not ok:
raise ctypes.WinError()
kernel32.AssignProcessToJobObject(job, hproc)
kernel32.CloseHandle(hproc)
return job # 生存を維持 — ジョブが閉じると(GC時)プロセスが終了する
# proc = subprocess.Popen(...) の後: job = restrict_process(proc.pid)
Tier 3 — Windows 沙盒(CI 完整 OS 隔离)¶
如果每次运行都需要干净的 Windows 镜像(无残留注册表项、无共享 GPU 状态、真正隔离),请在 Windows 沙盒 中运行整个测试套件。
要求: Windows 10/11 Pro 或企业版,并启用虚拟化。
在项目根目录创建 e2e-sandbox.wsb:
<Configuration>
<MappedFolders>
<!-- アプリバイナリ(読み取り専用) -->
<MappedFolder>
<HostFolder>C:\path\to\your\build\Release</HostFolder>
<SandboxFolder>C:\app</SandboxFolder>
<ReadOnly>true</ReadOnly>
</MappedFolder>
<!-- テストスイート(アーティファクト用に読み書き可能) -->
<MappedFolder>
<HostFolder>C:\path\to\your\e2e_test</HostFolder>
<SandboxFolder>C:\e2e_test</SandboxFolder>
<ReadOnly>false</ReadOnly>
</MappedFolder>
</MappedFolders>
<LogonCommand>
<!--
WindowsサンドボックスはデフォルトでPythonがない。まずサイレントインストール、
次に依存関係をインストールしてテストを実行する。アーティファクトは
上記のMappedFolderを通じてホストに書き戻される。
-->
<Command>powershell -Command "
winget install --id Python.Python.3.11 --silent --accept-package-agreements;
$env:PATH += ';' + $env:LOCALAPPDATA + '\Programs\Python\Python311\Scripts';
cd C:\e2e_test;
pip install -r requirements.txt;
pytest tests\ -v
"</Command>
</LogonCommand>
</Configuration>
启动:WindowsSandbox.exe e2e-sandbox.wsb
pywinauto 和应用都在沙盒内部运行(需要同一会话)。 测试产物会通过映射文件夹写回主机。
层级比较¶
| 层级 | 隔离 | 设置成本 | CI 中可用 | 使用时机 |
|---|---|---|---|---|
1 — tmp_path 环境重定向 |
文件系统 | 零 | 始终 | 所有测试的默认 |
| 2 — 作业对象 | 进程树 | 低 | 始终 | 防止子进程逃逸 |
| 3 — Windows 沙盒 | 完整 OS | 中 | 需要 Pro/Enterprise 镜像 | 定期洁净室运行 |
防止测试挂起¶
添加 pytest-timeout 为单个测试设置上限。在 pytest.ini 中设置 timeout = 60 和 timeout_method = thread。注意:thread 方法在 Windows 上无法终止 Qt 应用的子进程 — 请在 conftest.py 中添加 atexit.register(lambda: [p.kill() for p in psutil.Process().children(recursive=True)]) 以清理孤儿进程。
CI/CD 集成¶
# .github/workflows/e2e-desktop.yml
name: Desktop E2E
on: [push, pull_request]
jobs:
e2e:
runs-on: windows-latest # 実際のGUI環境、Xvfb不要
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- name: 依存関係をインストール
run: pip install pywinauto pytest pytest-html Pillow
- name: アプリをビルド
run: cmake --build build --config Release # ビルドシステムに合わせて調整
- name: E2Eを実行
env:
APP_PATH: ${{ github.workspace }}\build\Release\MyApp.exe
APP_TITLE: "My Application"
CI: "true"
run: pytest tests/ --html=artifacts/report.html --self-contained-html --junitxml=artifacts/results.xml -v
- uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-artifacts
path: artifacts/
retention-days: 14
Qt 特定¶
在 Qt 5.x 中启用 UIA¶
Qt 5.x 的辅助功能在某些构建(尤其是 5.7 到 5.14)中默认禁用。请在启动前设置环境变量。Qt 6.x 默认启用辅助功能 — 在 Qt 6 中请跳过此步骤。
或在 CI 中导出:
为 Qt 控件添加稳定标识符¶
// 優先:objectNameとaccessibleNameの両方
void setTestId(QWidget* w, const char* id) {
w->setObjectName(id);
w->setAccessibleName(id); // UIA Nameプロパティになる
}
// ダイアログコンストラクタ内:
setTestId(ui->usernameEdit, "usernameInput");
setTestId(ui->passwordEdit, "passwordInput");
setTestId(ui->loginButton, "btnLogin");
setTestId(ui->errorLabel, "lblError");
为避免拼写错误,请将所有 ID 集中到头文件中:
// test_ids.h
#define TID_USERNAME "usernameInput"
#define TID_PASSWORD "passwordInput"
#define TID_BTN_LOGIN "btnLogin"
#define TID_LBL_ERROR "lblError"
Qt 特定注意事项¶
QComboBox — 下拉框是另一个顶层窗口:
from pywinauto import Desktop
def select_combo_item(page, combo_spec, item_text):
page.click(combo_spec)
# ドロップダウンは新しいルートレベルウィンドウとして表示される
# class_nameはQtバージョンによって異なる — Accessibility Insightsで確認
# Qt 5.x: "Qt5QWindowIcon" | Qt 6.x: "Qt6QWindowIcon" — Accessibility Insightsで確認
popup = Desktop(backend="uia").window(class_name_re="Qt[56]QWindowIcon")
popup.wait("visible", timeout=5)
popup.child_window(title=item_text).click_input()
QMessageBox / QDialog — 这也是另一个顶层窗口:
dlg = page.wait_window("Confirm") # ダイアログタイトルを待つ
dlg.child_window(title="OK").click_input() # 内部のボタンをクリック
QTableWidget / QTableView — 行/单元格访问:
table = page.by_id("tblUsers").wrapper_object()
cell = table.cell(row=0, column=1)
print(cell.window_text())
自绘控件(仅 paintEvent、QGraphicsView、QOpenGLWidget)— UIA 无法查看其内部。请使用以下回退章节。
回退:截图模式¶
当控件无法通过 UIA 访问时(自绘、第三方、游戏引擎):
import pyautogui, cv2, numpy as np
from PIL import Image
def find_image_on_screen(template_path, confidence=0.85):
"""画面上のテンプレート画像を探す。(x, y) の中心またはNoneを返す。"""
screen = np.array(pyautogui.screenshot())
template = np.array(Image.open(template_path))
result = cv2.matchTemplate(
cv2.cvtColor(screen, cv2.COLOR_RGB2BGR),
cv2.cvtColor(template, cv2.COLOR_RGB2BGR),
cv2.TM_CCOEFF_NORMED,
)
_, max_val, _, max_loc = cv2.minMaxLoc(result)
if max_val >= confidence:
h, w = template.shape[:2]
return max_loc[0] + w // 2, max_loc[1] + h // 2
return None
def click_image(template_path, confidence=0.85):
pos = find_image_on_screen(template_path, confidence)
if pos is None:
raise RuntimeError(f"画面上で画像が見つからない:{template_path}")
pyautogui.click(*pos)
谨慎使用 — 图像匹配会因 DPI 变化、主题切换、部分遮挡而失效。 始终先尝试 UIA,仅对确实无法访问的控件回退到截图。
反模式¶
# BAD: 固定スリープ
time.sleep(3)
page.click(page.by_id("btnSubmit"))
# GOOD: 条件ウェイト
page.wait_visible(page.by_id("btnSubmit"))
page.click(page.by_id("btnSubmit"))
# BAD: 主要戦略として脆弱なクラス+インデックスロケーター
page.by_class("Edit", index=2).type_keys("hello")
# GOOD: AutomationId
page.by_id("usernameInput").set_edit_text("hello")
# BAD: ピクセル座標でのアサート
assert btn.rectangle().left == 120
# GOOD: コンテンツ/状態でのアサート
assert page.get_text(page.by_id("lblStatus")) == "Logged in"
assert page.by_id("btnLogout").is_enabled()
# BAD: 全テストにわたってアプリインスタンスを共有(状態の漏洩)
@pytest.fixture(scope="session")
def app(): ...
# GOOD: テストごとに新しいプロセス(または最大でもクラスごと)
@pytest.fixture(scope="function")
def app(): ...
运行测试¶
# 全テスト
pytest tests/ -v
# スモークのみ
pytest tests/ -m smoke -v
# 特定ファイル
pytest tests/test_login.py -v
# カスタムアプリパスで実行
APP_PATH="C:\build\Release\MyApp.exe" APP_TITLE="MyApp" pytest tests/ -v
# 不安定なテストを検出(各テストを5回繰り返す)
pip install pytest-repeat
pytest tests/test_login.py --count=5 -v
相关技能¶
e2e-testing— 使用 Playwright 进行 Web 应用程序 E2E 测试cpp-testing— 使用 GoogleTest 进行 C++ 单元/集成测试cpp-coding-standards— C++ 代码风格和模式
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC