Windows Application DriverでWindowsアプリケーションのテストを自動化しよう

  • 27
    いいね
  • 0
    コメント

Windows Application Driverとは?

https://github.com/Microsoft/WinAppDriver

MicrosoftがOSSとして開発しているAppiumDriverの1つで、AppiumやSelenium互換のAPIでWindowsアプリケーション(WinFormsからWPF, ストアアプリまで)を操作できます。

これまでWindowsアプリケーションの自動テストには高価な商用ツールか商用テクニックや知識が必要なOSSが必要でしたが、Windows Application DriverはSeleniumの知識と経験を活かすことができます。またMicrosoft自体が開発の中心になっているため、今後のサポートも安心です。さらにAppium 5系からはWindows Application DriverがDriverの1つとして正式に取り込まれており、今後はWindowsアプリケーションの自動化のデファクトとなる可能性が高いです。

Javaのサンプルを実行してみる

事前準備として、下記から最新のWindows Application Driverをダウンロードしてインストールし、実行します。

https://github.com/Microsoft/WinAppDriver/releases

その後、git cloneしてサンプルを実行してみます。サンプルにはC#, Python, Ruby, Javaのバインディングがあります。MicrosoftだけあってC#がメモ帳から電卓、Cortanaなど複数充実していますが、ここではSelenium人口が多いJavaのサンプルを実行してみます。

OSはWindows 10 Professional日本語版, Creators Update適用後です。JavaプロジェクトはMavenプロジェクトになっていますので、JDKとMavenが必要です。

> git clone https://github.com/Microsoft/WinAppDriver.git
> cd WinAppDriver\Samples\Java\CalculatorTest
> mvn test
Tests run: 5, Failures: 0, Errors: 5, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

ふぁ><テストが全部Failしましたね。実はこのJavaプロジェクトを含めWindows Application Driverのサンプルはロケーターの国際化対応がされていないので、日本語環境ではテストがFailします。

国際化対応のためにコードを修正します。その前にpom.xmlを修正して、最新のAppiumクライアントを使用するようにします。これによって、クライアント側もWindows Application Driverを使えるようになります。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>CalculatorTest</groupId>
    <artifactId>CalculatorTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.source>${java.version}</maven.compiler.source>
    </properties>

    <dependencies>
       <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>io.appium</groupId>
            <artifactId>java-client</artifactId>
            <version>5.0.0-BETA7</version>
        </dependency>
    </dependencies>

</project>

ロケーターをAccessibilityId(後述のinspectではAutomationId)を使うように変更します。Webと同じくお行儀の良いWindowsアプリケーションには必ずAutomationIdがついているそうです。対象の電卓もWindows付属のアプリケーションと言うこともありもちろんすべてAutomationIdがついています。

変更点は以下の3つです。

  • ロケーターをOSの言語設定に依存しないAutomationIdを使うように修正
  • 部分一致などマッチャーが充実しているassertThatを使うように変更
  • テキストのアサーションをどの言語でも(恐らく)大丈夫なように数値文字列の部分一致に変更
CalculatorTest.java
//******************************************************************************
//
// Copyright (c) 2016 Microsoft Corporation. All rights reserved.
//
// This code is licensed under the MIT License (MIT).
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//******************************************************************************

import io.appium.java_client.windows.WindowsDriver;
import org.junit.*;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;

import java.util.concurrent.TimeUnit;
import java.net.URL;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;


public class CalculatorTest {

    private static WindowsDriver calculatorSession = null;
    private static WebElement calculatorResult = null;

    @BeforeClass
    public static void setup() throws Exception {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
        calculatorSession = new WindowsDriver(new URL("http://127.0.0.1:4723"), capabilities);
        calculatorSession.manage().timeouts().implicitlyWait(2, TimeUnit.SECONDS);

        calculatorSession.findElementByAccessibilityId("clearButton").click();
        calculatorSession.findElementByAccessibilityId("num7Button").click();
        calculatorResult = calculatorSession.findElementByAccessibilityId("CalculatorResults");
        assertThat(calculatorResult, is(notNullValue()));
    }

    @Before
    public void Clear() {
        calculatorSession.findElementByAccessibilityId("clearButton").click();
        assertThat(calculatorResult.getText(), is(containsString(" 0 ")));
    }

    @AfterClass
    public static void TearDown() {
        calculatorResult = null;
        if (calculatorSession != null) {
            calculatorSession.quit();
        }
        calculatorSession = null;
    }

    @Test
    public void Addition() {
        calculatorSession.findElementByAccessibilityId("num1Button").click();
        calculatorSession.findElementByAccessibilityId("plusButton").click();
        calculatorSession.findElementByAccessibilityId("num7Button").click();
        calculatorSession.findElementByAccessibilityId("equalButton").click();
        assertThat(calculatorResult.getText(), is(containsString(" 8 ")));
    }

    @Test
    public void Combination() {
        calculatorSession.findElementByAccessibilityId("num7Button").click();
        calculatorSession.findElementByAccessibilityId("multiplyButton").click();
        calculatorSession.findElementByAccessibilityId("num9Button").click();
        calculatorSession.findElementByAccessibilityId("plusButton").click();
        calculatorSession.findElementByAccessibilityId("num1Button").click();
        calculatorSession.findElementByAccessibilityId("equalButton").click();
        calculatorSession.findElementByAccessibilityId("divideButton").click();
        calculatorSession.findElementByAccessibilityId("num8Button").click();
        calculatorSession.findElementByAccessibilityId("equalButton").click();
        assertThat(calculatorResult.getText(), is(containsString(" 8 ")));
    }

    @Test
    public void Division() {
        calculatorSession.findElementByAccessibilityId("num8Button").click();
        calculatorSession.findElementByAccessibilityId("num8Button").click();
        calculatorSession.findElementByAccessibilityId("divideButton").click();
        calculatorSession.findElementByAccessibilityId("num1Button").click();
        calculatorSession.findElementByAccessibilityId("num1Button").click();
        calculatorSession.findElementByAccessibilityId("equalButton").click();
        assertThat(calculatorResult.getText(), is(containsString(" 8 ")));
    }

    @Test
    public void Multiplication() {
        calculatorSession.findElementByAccessibilityId("num9Button").click();
        calculatorSession.findElementByAccessibilityId("multiplyButton").click();
        calculatorSession.findElementByAccessibilityId("num9Button").click();
        calculatorSession.findElementByAccessibilityId("equalButton").click();
        assertThat(calculatorResult.getText(), is(containsString(" 81 ")));
   }

    @Test
    public void Subtraction() {
        calculatorSession.findElementByAccessibilityId("num9Button").click();
        calculatorSession.findElementByAccessibilityId("minusButton").click();
        calculatorSession.findElementByAccessibilityId("num1Button").click();
        calculatorSession.findElementByAccessibilityId("equalButton").click();
        assertThat(calculatorResult.getText(), is(containsString(" 8 ")));
    }
}

再度実行します。

> mvn test
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

見事全グリーンになりました。

Javaのコードを見ると、Selenium互換のAPIでWindowsのアプリケーションを操作できていることが分かります。ダイアログを待つなど他のコンポーネントがでてくる場合の処理はC#のメモ帳のサンプルが参考になりますが、Sleepしていたりして正しい実装とは言えないので、wait.untilを適切に使うか、SelenideやGebへの組み込みに期待したいと思います。

とはいえ、Javaのサンプルが動かない事を始め、ちょっとDriver自体の素晴らしさに対してサンプルが非常に残念なので修正版をプルリクしたいと思います。

inspectについて

WebアプリケーションではFirefoxやChromeの開発ツールがロケーターを調査するのに非常に役立ちますが、Windowsアプリケーションでそれに当たるのが、inspect.exe です。Windows SDKで単独でインストールすることもできますが、Visual Studioのインストールで一緒にインストールしてしまうのが簡単です。

inspect.exeを起動して、自動テストの対象のアプリケーションを起動し、ロケーターを見たい要素にフォーカスを当てると、inspect.exeで要素の詳細情報が表示されます。AutomationId以外にも様々なロケーターが使用できますが、AutomationIdはOSの言語設定によらずアプリケーションで一意なのでWebアプリケーションのidと同じように信頼して使用することができます。