Source profileQuality 77/100Review permissions

affaan-m/ECC/docs/ja-JP/skills/windows-desktop-e2e/SKILL.md

windows-desktop-e2e

E2E testing for Windows native desktop apps (WPF, WinForms, Win32/MFC, Qt) using pywinauto and Windows UI Automation.

Source repository stars
234,327
Declared platforms
0
Static risk flags
2
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

pywinauto と Windows UI Automation(UIA)を使用したWindowsネイティブデスクトップアプリケーションのエンドツーエンドテスト。WPF、WinForms、Win32/MFC、Qt(5.x / 6.x)をカバーし、Qt固有のガイダンスは専用セクションとして提供します。

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "docs/ja-JP/skills/windows-desktop-e2e"
    Safe inspection promptEditorial

    Inspect the Agent Skill "windows-desktop-e2e" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/docs/ja-JP/skills/windows-desktop-e2e/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      アクティベートするタイミング

      Windowsネイティブデスクトップアプリケーションのエンドツーエンドテストを書くまたは実行するとき

      Windowsネイティブデスクトップアプリケーションのエンドツーエンドテストを書くまたは実行するときデスクトップGUIテストスイートをゼロから設定するとき不安定または失敗するデスクトップオートメーションテストを診断するとき
    2. 02

      使用しないタイミング

      Webアプリケーション → e2e-testing スキル(Playwright)を使用する

      Webアプリケーション → e2e-testing スキル(Playwright)を使用するElectron / CEF / WebView2 アプリ → HTMLレイヤーにはUIAではなくブラウザオートメーションが必要モバイルアプリ → プラットフォーム固有のツールを使用する(UIAutomator、XCUITest)
    3. 03

      コアコンセプト

      すべてのWindowsデスクトップオートメーションはUI Automation(UIA)に依存します。これはWindowsに組み込まれたアクセシビリティAPIです。サポートされているすべてのフレームワークは、読み取りおよび操作可能なプロパティを持つUIA要素のツリーを公開します:

      すべてのWindowsデスクトップオートメーションはUI Automation(UIA)に依存します。これはWindowsに組み込まれたアクセシビリティAPIです。サポートされているすべてのフレームワークは、読み取りおよび操作可能なプロパティを持つUIA要素のツリーを公開します:
    4. 04

      セットアップと前提条件

      Review the “セットアップと前提条件” section in the pinned source before continuing.

      Review and apply the “セットアップと前提条件” source section.
    5. 05

      Python 3.8+、Windowsのみ

      pip install pywinauto pytest pytest-html Pillow pytest-timeout

      pip install pywinauto pytest pytest-html Pillow pytest-timeout

    Permission review

    Static risk signals and limitations

    Runs scripts

    medium · line 47

    The documentation asks the agent to run terminal commands or scripts.

    # Python 3.8+、Windowsのみ

    Network access

    medium · line 50

    The documentation includes network, browsing, or remote request actions.

    # ffmpegをインストールしてPATHに追加:https://ffmpeg.org/download.html

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score77/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    docs/ja-JP/skills/windows-desktop-e2e/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    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信頼性注記
    WPF5/5優秀x:Name が直接AutomationIdにマッピング
    WinForms4/5良好AccessibleName = AutomationId
    UWP / WinUI 35/5優秀Microsoftの完全サポート
    Qt 6.x5/5優秀アクセシビリティがデフォルトで有効;クラス名が Qt6* に変更
    Qt 5.15+4/5良好Accessibilityモジュールが改善
    Qt 5.7–5.143/5普通QT_ACCESSIBILITY=1 が必要;objectNameは手動設定
    Win32 / MFC3/5普通コントロールIDにアクセス可能;テキストマッチングが一般的

    セットアップと前提条件

    # Python 3.8+、Windowsのみ
    pip install pywinauto pytest pytest-html Pillow pytest-timeout
    # オプション:画面録画
    # ffmpegをインストールしてPATHに追加:https://ffmpeg.org/download.html
    

    UIAが到達可能か確認:

    from pywinauto import Desktop
    Desktop(backend="uia").windows()  # すべてのトップレベルウィンドウを一覧表示
    

    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サンドボックスフィクスチャ(以下参照)を優先してください — 追加コストゼロでファイルシステムの分離が追加されます。この基本フィクスチャは最小限/レガシーセットアップ専用です。

    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
    

    ロケーター戦略

    AutomationId  >  Name(テキスト)  >  ClassName + インデックス  >  XPath
      (安定)         (可読)            (脆弱)                   (最後の手段)
    

    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.sleepwait_visible に置き換える
    ウィンドウがフォーカスされていないインタラクション前に win.set_focus() を追加する
    アニメーション進行中wait_until(lambda: not loading_indicator.exists())
    ダイアログのタイミングwait_window(title, timeout=15)
    CI環境のディスプレイが準備できていないDISPLAY を設定するかCIで仮想デスクトップを使用する

    テスト分離とサンドボックス

    分離の3つの階層 — ニーズを満たす最も軽い階層を使用してください。

    Tier 1 — ファイルシステム分離(デフォルト、常に使用)

    各テストは subprocess.PopenApplication.connect() を通じて独自の APPDATA / LOCALAPPDATA / TEMP を取得します。pytestの tmp_path フィクスチャがクリーンアップを自動的に処理します。

    # 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サンドボックス完全OSPro/Enterpriseイメージが必要定期的なクリーンルーム実行

    テストのハングを防止する

    pytest-timeout を追加して単一テストに上限を設けます。pytest.initimeout = 60timeout_method = thread を設定します。注意:thread メソッドはWindows上でQtアプリのサブプロセスを終了できません — conftest.pyatexit.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ではこのステップをスキップしてください。

    # conftest.py — モジュールの先頭に追加
    import os
    os.environ["QT_ACCESSIBILITY"] = "1"
    

    またはCIでエクスポート:

    env:
      QT_ACCESSIBILITY: "1"
    

    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のみ、QGraphicsViewQOpenGLWidget)— UIAは内部を見ることができません。以下のフォールバックセクションを使用してください。

    フォールバック:スクリーンショットモード

    コントロールがUIAで到達できない場合(自己描画、サードパーティ、ゲームエンジン):

    pip install pyautogui Pillow opencv-python
    
    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 — WebアプリケーションのPlaywright E2Eテスト
    • cpp-testing — GoogleTestを使用したC++ユニット/統合テスト
    • cpp-coding-standards — C++コードスタイルとパターン

    Alternatives

    Compare before choosing