Windows Application Driver を使った Windows ネイティブアプリケーションの UI テスト自動化

2020/3/8 追記

- 長らく放置していたため、リンク切れしていた箇所を修正しました。
- サポート期間終了に伴い、Windows 7、8 の記載を削除しました。
- UI エレメントの調査用のツールが増えていたので、記載を拡充しました。
- [General Development & Best Practices](https://github.com/microsoft/WinAppDriver/blob/master/Docs/FAQ.md#general-development--best-practices)について最後に追記しました。
- 現在は Windows 用の Appium から Windows Application Driver を呼び出すことができるため、改題しました。
    - 本記事は Windows App Driver 単体での利用について記載しています。

---

# Windows Application Driver について

Android / iOS アプリの自動テストで有名な Appium ですが、2017年11月 にはこれまで長らくベータ提供だった Windows Application Driver が  Microsoft より正式にリリースされました。
　[Windows Blog：Windows Application Driver を正式にリリース](https://blogs.windows.com/japan/2017/11/09/windows-application-driver-no-longer-beta/)

これまでも UWSC などを使って Windows ネイティブアプリを自動で操作することはできましたが、今回のリリースによって Selenium(Web アプリ)、Appium(スマホアプリ)で得た知見をそのまま Windows ネイティブアプリの UI テストにも生かすことができます。
これまで Selenium や Appium を触ったことがない人でも Java や Python など自分の慣れた言語でテストケースが書けるという大きなメリットがありますし、JUNIT などの既存のフレームワークも流用できます。
※なお、対応言語は Selenium / Appium に準じています。対応言語は[こちら](http://www.selenium.jp/selenium-and-appium)を参照してください。

以下、GitHub に公開されている [WinAppDriver](https://github.com/Microsoft/WinAppDriver/blob/master/README.md#Authoring%20Your%20Own%20Test%20Script) の [README.md](https://github.com/Microsoft/WinAppDriver/blob/master/README.md) を元にご紹介していきます。
※2018年7月時点での内容です。リポジトリが更新されていくにつれ内容が乖離する可能性がありますので、正確なところは [WinAppDriver のリポジトリ](https://github.com/Microsoft/WinAppDriver/blob/master/README.md#Authoring%20Your%20Own%20Test%20Script)を参照してください。

## よくある質問
[FAQ](https://github.com/Microsoft/WinAppDriver/wiki/Frequently-Asked-Questions) をご確認ください。

## システム要件

- Windows 10 PC
- Appium のテストランナー
    - これは Microsoft Visual Studio に限らず、使い慣れたものを使うのが良いでしょう。私は Python で実装しているので、VScode と Python 向けのプラグインを使っています。

なお、Appium のドキュメントの方には書いてあるのですが、管理者権限で実行できることが要件になります。
[The Windows Driver: requirements-and-support](http://appium.io/docs/en/drivers/windows/#requirements-and-support)
また、このドキュメントの [Setup](http://appium.io/docs/en/drivers/windows/#setup) に記載されている通り、始めに Windows の開発者モードを ON にする必要があります。

## Windows Application Driver のインストールと起動
まず、GitHub から Windows Application Driver をダウンロードします。
https://github.com/Microsoft/WinAppDriver/releases
2018年7月現在の最新は v1.1 Preview Release、最新の Stable は v1.0 Release です。お好きな方の .msi ファイルをダウンロード、インストールしてください。

インストール後、コマンドプロンプトから WinAppDriver.exe を実行してください。
デフォルトパスにインストールした場合は以下のコマンドになります。

```shell-session
"C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe"
Windows Application Driver listening for requests at: http://127.0.0.1:4723/ #コマンド実行後にこれが表示されれば OK
Press ENTER to exit.
```

デフォルトだと Localhost のポート 4723 で起動しますが、IP、ポート番号のいずれも起動時の引数で指定することが可能です。

```shell-session
WinAppDriver.exe 4727
WinAppDriver.exe 10.0.0.10 4725
WinAppDriver.exe 10.0.0.10 4723/wd/hub
```
※ デフォルトとは異なる IP/ポートを使用する場合は、WinAppDriver.exe を管理者モードで実行してください。

# 自動テストの作成
## テストのサンプル
Alarms & Clock, Calculator, and Notepad などの Windows 10 ビルトインアプリを対象とした各言語でのサンプルコードは[こちら](https://github.com/Microsoft/WinAppDriver/tree/master/Samples)です。

アプリの起動や各エレメントの見つけ方、クリック、キーストローク、テキストの読み取りなどの色んなバリエーションがあるそうです(あんまり見ていない)。

## テストケースの実装

[Authoring Your Own Test Script](https://github.com/microsoft/WinAppDriver/blob/master/Docs/AuthoringTestScripts.md) では C# でのテストコードの書き方が説明されています。

私は Python を使っていますが、API などの名称が大文字、小文字で変わったりする程度で、基本的な実装は変わりません。
例えば『Testing a Universal Windows Platform Application』のソースコードを Python で書くと

```python:uwpa_test.py
from appium import webdriver

# Launch the Alarms & Clock app
desired_caps = {}
desired_caps["app"] = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"
alarm_clock_session = webdriver.Remote(
    command_executor='http://127.0.0.1:4723',
    desired_capabilities= desired_caps)


# Use the session to control the app
alarm_clock_session.find_element_by_accessibility_id("AddAlarmButton").click();
alarm_clock_session.find_element_by_accessibility_id("AlarmNameTextBox").clear();
```

ですし、『Testing a Classic Windows Application』を Python で書くと

```python:cwa_test.py
from appium import webdriver

# Launch Notepad
desired_caps = {}
desired_caps["app"] = r"C:\Windows\System32\notepad.exe"
desired_caps["appArguments"] = "MyTestFile.txt"
desired_caps["appWorkingDir"] = r"C:\MyTestFolder" + "\\"
notepad_session = webdriver.Remote(
    command_executor='http://127.0.0.1:4723',
    desired_capabilities= desired_caps)

# Use the session to control the app
notepad_session.find_element_by_class_name("Edit").send_keys("This is some text");
```
こうです。
実行に必要なライブラリは pip install [robotframework-appiumlibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary) でインストールできます。

UWPA はアプリケーションの ID を指定するだけで実行できますが、従来の Windows アプリケーションの場合は実行時コマンドのフルパスで指定する必要があります。いずれの場合でも appArguments で起動時の引数を指定することが可能ですが、appWorkingDir の指定は従来の Windows アプリケーションのみ有効なようです。

DesiredCapabilities という JSON オブジェクトが急に出てきましたが、ここら辺を正確に理解するには [Appium のドキュメント](http://appium.io/docs/en/writing-running-appium/caps/)を参照してください。

## Supported Capabilities

|Capabilities|Descriptions|Example|
|---|---|---|
|app|Application identifier or executable full path|Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge|
|appArguments|Application launch arguments|https://github.com/Microsoft/WinAppDriver
|appTopLevelWindow|Existing application top level window to attach to|0xB822E2|
|appWorkingDir|Application working directory (Classic apps only)|C:\Temp|
|platformName|Target platform name|Windows|
|platformVersion|Target platform version|1|

DesiredCapabilities で設定できる Capabilities の一覧が紹介されています。
先の例で desired_caps["app"] = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App" と指定していたのが、この一覧でいう app という Capabilities です。

## UI エレメントを指定するための Supported Locators 
|Client API|Locator Strategy|Matched Attribute in inspect.exe|Example|
|---|---|---|---|
|FindElementByAccessibilityId|accessibility id|AutomationId|AppNameTitle|
|FindElementByClassName|class name|ClassName|TextBlock|
|FindElementById|id|RuntimeId (decimal)|42.333896.3.1|
|FindElementByName|name|Name|Calculator|
|FindElementByTagName|tag name|LocalizedControlType (upper camel case)|Text|
|FindElementByXPath|xpath|Any|//Button[0]|

Appium では操作する UI エレメントを指定する際に、その ID や名前を事前に調べておき、それらを指定する必要があります。
先の例で alarm_clock_session.find_element_by_accessibility_id("AddAlarmButton") と指定している箇所では、Alarms & Clock アプリの UI のうち、新しいアラームを追加するボタンを AddAlarmButton という accessibility id で指定しています。

## UI エレメントを調べる

### inspect.exe

上記の UI エレメントはテストケースを実装する際に調べておく必要がありますが、最新の Microsoft Visual Studio に含まれる Windows SDK の中にはこれを調べられる inspect.exe というツールがあります。
使い方については、[MSDN のドキュメント](https://docs.microsoft.com/ja-jp/windows/desktop/WinAuto/inspect-objects)を参照してください。

Windows SDK をインストールされている場合は C:\Program Files (x86)\Windows Kits\XX\bin\x86 に配置されています。
※XX は番号で、8.0 以降であれば inspect.exe が同梱されているようです。

Windows SDK をインストールしていない場合は以下のリンクなどからダウンロードしてインストールしてください。
https://developer.microsoft.com/en-us/windows/downloads/sdk-archive

inspect.exe を実行すると現在実行中のアプリが全てアプリの左側にツリー表示されるので、特にドキュメントを調べなくても使い方は直感的に分かると思います。
<img src="https://qiita-image-store.s3.amazonaws.com/0/135861/cd4459e9-345f-c937-d13d-313ef8bcca00.png" width=50%>

### UI Recorder

[UI Recorder](https://github.com/microsoft/WinAppDriver/blob/master/Docs/UsingUIRecorder.md) という便利なツールがあるようです。UI の動きを記録して C# のコードまで出してくれるので、C# 使いはこれを採用するのが良さそうです。

ただし、同様の機能を備える下記の appium-desktop では、自動生成されるコードは本番用のテストコードではなく、あくまで対応している Appium ライブラリをコードで教えてくれる学習ツールである、と補足がありました。
実際に見てはいませんが、自動生成されたコードは使用する前に開発者の方で精査、必要に応じて修正するのが良いと思います。

### Appium Desktop

[Appium Desktop](https://github.com/appium/appium-desktop) は Mac、Windows、Linux 上で動作するデスクトップアプリケーションです。
これに [Recorder](https://github.com/appium/appium-desktop#the-recorder) の機能があるため、これを使って UI エレメントを調べることができます。

しかし、Windows Application Driver をインストールしている環境で UI エレメントを調べるためだけに Appium Desktop をわざわざインストールするのはオーバースペックであり、過剰です。
※ クロスプラットフォームで使っている場合などは、採用しても良いと思います。

---

クラシックな Windows アプリでは一般的な操作にはショートカットキーが割り当てられているので、UI エレメントを指定せずに send_keys でショートカットキーを実行することで済ませることもできます。

コンパクトに、簡単に実装できるので簡単な検証用のコードとしては良いかもしれませんが、この方法ではコードの可読性も含めた保守性に難があるかもしれません。
また、テスト実行中に別プロセスがウィンドウを立ち上げた時などに影響を受けそうです。

# おまけ： スクリーンショットのサンプル

一般的な UI テストの自動化であれば、アプリケーションに対して特定の操作を行なった後にスクショを撮るのが基本的な流れになると思います。
先ほどの Alarms & Clock アプリを例に、自動実行結果のスクリーンショットを取得するコードをおまけに記載しておきます。

```python:screenshot.py
from appium import webdriver

# Launch the Alarms & Clock app
desired_caps = {}
desired_caps["app"] = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"
alarm_clock_session = webdriver.Remote(
    command_executor='http://127.0.0.1:4723',
    desired_capabilities= desired_caps)

# Use the session to control the app
alarm_clock_session.find_element_by_accessibility_id("AddAlarmButton").click();
alarm_clock_session.find_element_by_accessibility_id("AlarmNameTextBox").clear();

# Take and save screenshot
screenshot = alarm_clock_session.get_screenshot_as_png()
with open(r"C:\MyTestFolder\add_alarm.png", "wb") as fout:
    fout.write(screenshot)

# Quit app
alarm_clock_session.quit()
```

上記のコードを実行すると、Alarms & Clock アプリが自動実行されたウィンドウのスクリーンショットが C:\MyTestFolder\add_alarm.png として保存されます。

<div align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/135861/8afcafea-23f5-ee77-1aa1-d5ff23257e88.png" width=40%>
</div>

---

さまざまなテクニックは [General Development & Best Practices](https://github.com/microsoft/WinAppDriver/blob/master/Docs/FAQ.md#general-development--best-practices) にもまとめられているので、ご参照ください。