WinnAppDriverの要素取得速度 - どちらが速い？

# 要旨
　様々なツールの登場によって、ソフトウェアテストの自動化がどうも容易になってきたらしい。しかし、時間は有限である。
特に、対象のソフト内の要素を取得するステップは1つのテスト内で大きな時間を要するだろう。
　この要素取得はおおまかに分けて3通り考えられる。

- [案1] その要素を名前やAutomationIDなど、要素と一対一対応する値で取得する方法（下の絵, 案1）
- [案2] 要素の系統樹を上からたどるように1つずつ取得して、最終的に目的の要素を取得する方法（下の絵, 案2）
- [案3] 予め一度だけ数段上の要素を取得しておき、その要素の下で目的の要素を取得する方法（下の絵, 案3）

　探索範囲で見れば、案1＞案3＞案2の順に大きいだろうから、案2が最速な気がする。では実際どうか？
　この記事では、Windows Application Driver (WinAppDriverとも)による自動テストの大部分を占めるソフトの要素取得に要する時間が、その取得法によってどう変化するかを調べた結果を示す。試した結果、**中間の要素を1段ずつ取得する案2は案1よりわずかに遅くなり、案3が圧倒的に最速なことが分かった**。案2では所要時間を改善できない上に細かすぎるため、ソフトのアップデートで中間要素が変わった場合にメンテナンスが苦行になるという点で有用と言えない。また、GUIのアップデートに対応するのがそんなに苦ではないラインを見極めて案3を取るのが良いだろう。GUIのアップデートが激しい開発中の段階ならば、開発チームにAutomationIDの付与をお願いして案1を取るのが良いだろう。

![QiitaWinAppDriver.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138900/b9254335-c5e2-464b-a27c-94010b91870b.png)

#環境
- テスト対象のソフト : Excel
- テストコードはC#で記述してテスト実行にはNUnit
- WinAppDriver v1.2
- NuGetから
     - Microsoft.WinAppDriver.Appium.WebDriver - WinAppDriver動かすため
     - NUnit - テスト実行
     - Nunit3TestAdapter - テスト実行
     
Microsoft.WinAppDriver.Appium.WebDriverはAppium v.3系を使ってて古いです。ですが環境構築が楽なので。
　コーディングと実行はVisual Studioを使用した。

#検証手順
　ソフトウェアテストのデモとして、Excelの「中央揃えボタン」を取得してクリックするテストを実行する。
その取得法を上記の3種類で行った。所要時間を比較するため、要素取得のステップを10回繰り返している。
　テストを行うためのセッション形成はSession.cs、テストの実体はTest.csである。コードは後に示す。
　実は上の図が、今回実験で行う実際の要素取得手順である。Testメソッド名も案1~3にしてある。

　Excelとのセッション形成は、[WinAppDriverのサンプルコード](https://github.com/microsoft/WinAppDriver/blob/master/Samples/C%23/CortanaTest/BingSearch.cs)を参考にして

```C#:Session.cs
using System;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Remote;

namespace GetElementSpeedTest
{
    public class Session
    {
        private const string winAppDriverURL = "http://127.0.0.1:4723";

        protected static WindowsDriver<WindowsElement> desktopSession;
        protected static WindowsDriver<WindowsElement> session;

        public static void Setup()
        {
            if (session == null)
            {
                // Desktopとsessionとる
                DesiredCapabilities desktopCapabilities = new DesiredCapabilities();
                desktopCapabilities.SetCapability("app", "Root");
                desktopSession = new WindowsDriver<WindowsElement>(new Uri(winAppDriverURL), desktopCapabilities);

                WindowsElement excelWindow = desktopSession.FindElementByName("Book1 - Excel");
                string excelTopLevelWindowHandle = (int.Parse(excelWindow.GetAttribute("NativeWindowHandle"))).ToString("x");

                // 起動済みExcelとsessionとる
                DesiredCapabilities appCapabilities = new DesiredCapabilities();
                appCapabilities.SetCapability("appTopLevelWindow", excelTopLevelWindowHandle);
                session = new WindowsDriver<WindowsElement>(new Uri(winAppDriverURL), appCapabilities);

                // Set implicit timeout to 1.5 seconds to make element search to retry every 500 ms for at most three times
                session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1.5);
            }
        }


        public static void TearDown()
        {
            // Close the application and delete the session
            if (session != null)
            {
                session = null;
            }
        }
    }
}

```
とした。
そしてテスト実行のコードは

```C#:Test.cs
using System;
using OpenQA.Selenium.Appium;
using System;
using OpenQA.Selenium.Appium;
using NUnit.Framework;

namespace GetElementSpeedTest
{
    [TestFixture]
    public class Test : Session
    {
        [Test]
        public static void ConstTest()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            // end speed test

            targetButton.Click();

            TearDown();
        }

        [Test]
        public void 案1()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            for (int i = 0; i < 10; i++)
            {
                targetButton = mainWindow
                    .FindElementByName("中央揃え");
            }
            // end speed test

            targetButton.Click();

            TearDown();
        }

        [Test]
        public void 案2()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            for (int i = 0; i < 10; i++)
            {
                targetButton = mainWindow
                    .FindElementByName("下リボン")
                    .FindElementByName("ホーム")
                    .FindElementByName("配置")
                    .FindElementByName("中央揃え");
            }
            // end speed test

            targetButton.Click();

            TearDown();
        }

        [Test]
        public void 案3()
        {
            Setup();
            AppiumWebElement mainWindow = session.FindElementByName("Book1 - Excel");
            AppiumWebElement ribbon = mainWindow.FindElementByName("下リボン");
            AppiumWebElement targetButton = mainWindow.FindElementByName("中央揃え");

            // begin speed test
            for (int i = 0; i < 10; i++)
            {
                targetButton = ribbon.FindElementByName("中央揃え");
            }
            // end speed test

            targetButton.Click();

            TearDown();
        }
    }
}
```
である。`ConstTest()`は`案n()`（$n=1, 2, 3$）の動作時間から共通部分に掛かる時間を差し引くための物である。

#結果
　実行時間はVisual Studioのテストエクスプローラーに表示された物を使用した。結果は下表の通り。試行回数は3である。単位は秒である。

| 案1  |     案2 |    案3  |
|:------------------:|:------------------:|:------------------:|
| 0.86 ± 0.13     |       0.95 ± 0.11 |      0.073 ± 0.012    |

　つまり、予想に反して案3が1桁少ない高速さを誇り、案2がわずかな差ではあるが最も遅かった。

#結語
　やはり最初に探索範囲を絞ってから要素を取得した案3が爆速であった。だからといって、Findの度に中間要素を細々取得する案2は改善しない上に保守性が劣悪なので悪手だろう。多分FineBy系のメソッドはそれ自体が時間を食うから案2がダメだったんでしょうね。案1は確かに遅いが、AutomationIDがあったり、Nameが奇跡的にuniqueで多言語対応しなくて良いならGUIの変更に対して最強である（このNameに関する仮定は万に一つ無いだろうが）。
　しかし、案2を取らざるを得ない状況が存在する。目的要素にAutomationIDが無く、その他の値でも一意に取得できない時である。そんなときはさっさとAutomatinIDを付けましょう。たとえその要素の使用頻度が低くても。

　要旨のセクションにも書いたが、中間要素をいっぱい取る気遣いをしたところで、そのボタンの場所が階層レベルで変更されたら即死するので使い物にならなくなる。ソフトのGUIはバンバン変更される物だろうから、メンテに時間を掛けなくて良いようにやっていきましょう。この塩梅をうまく見極めて案3を使えるのが理想だろう。それでも仕様変更が盛んな時において役立つのがAutomationIDだ。このメリットに言及している記事は多くある。まずAutomationID頼りで案1で行くのが堅いだろう。
　ということで、実行速度は
　案3>>案1＞案2 の順で速く、
　保守性は
　案1>>案3>>案2 の順で良いと考えられる。