こんにちは、キャスレーコンサルティングのSI(システム・インテグレーション)部:藤沢です。
今回は、UI Automationを使用してC#のアプリケーションのUI部分のテストの自動化を行います。

UI Automationは、デスクトップ上のほとんどの user interface (UI) 要素へのプログラムによるアクセスを提供し、
スクリーン リーダーなどの補助技術製品が UI に関する情報をエンド ユーザーに提供したり、
標準入力方式以外の方法で UI を操作したりできるようにします。
また、UI オートメーションは、自動テスト スクリプトが UI と対話できるようにします。(MSDNより)
この機能を利用することでアプリケーションのUIを操作が行えます。

今回の環境

  • Windows10
  • Visual Studio 2015
  • テストアプリケーション(WPF, .Net FrameWork4.5)
  • NUnit(GuNet)

テストアプリケーション

簡単な電卓のようなアプリケーションを作成し、そのUI部のテストを行います。
簡単な電卓の仕様として、テキストボックス(2個)とコンボボックス(演算子)と
ボタンで解答を求めるアプリケーションにします。
下記のような画面になっています。

UIAutomation_01

テストプロジェクト

VisualStudioの拡張機能を使用してVS上でテスト実行します。
そのため、「NUnit3 Test Adapter」をインストールします。

「ツール」 – 「拡張機能と更新プログラム」から「NUnit3 Test Adapter」を検索
UIAutomation_02

テストプロジェクトを作成します。
「ファイル」-「追加」-「新しいプロジェクト」から「単体テストプロジェクト」を選択し作成します。

UIAutomation_03

この作成したプロジェクトでNUnitを使用できるようにするため、NuGetパッケージマネージャーもしくは、
パッケージマネージャーコンソールからNuGetからNUnitをインストールします。

UIAutomation_04

このテストプロジェクトにUI Automationを使用するための参照を追加します。
追加するアセンプリは、「UIAutomationClient」「UIAutomationTypes」を選択します。
UIAutomation_05

テストシナリオ・テストコード

  • 入力テキストボックスに値が正しく入力されるか。
  • テキストボックスに「1234」と「5678」を入力し、その2つを減算した時の解答が正しく表示されるか。
using NUnit.Framework;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Automation;

namespace SimpleCalcTest
{
    [TestFixture]
    public class SimpleCalc
    {
        /// <summary>
        /// テスト対象EXE
        /// </summary>
        private string exePath = Path.Combine(”****”, "****.exe");

        /// <summary>
        /// UI Automation要素
        /// </summary>
        private AutomationElement aeWindow;

        /// <summary>
        /// テストセットアップ
        /// サンプル対象アプリケーションを起動
        /// </summary>
        [SetUp]
        public void SetUp()
        {
            // アプリケーションを起動
            var app = Process.Start(exePath);

            // 起動するまで待つ(1秒)
            Thread.Sleep(1000);

            aeWindow = AutomationElement.FromHandle(app.MainWindowHandle);
        }

        /// <summary>
        /// 終了
        /// テスト対象アプリケーションを終了
        /// </summary>
        [TearDown]
        public void TearDown()
        {
            var wp = aeWindow.GetCurrentPattern(WindowPattern.Pattern) as WindowPattern;
            wp.Close();
        }

        /// <summary>
        /// テストメソッド
        /// テキストボックスAに"123456789"を入力し正しいか
        /// </summary>
        [Test]
        public void TestTextBoxA()
        {
            var inputText = "123456789";

            // テキストボックスAに入力し入力が正しいか
            var inputA = findElementById(aeWindow, "txtA");
            var vpTextA = inputA.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            vpTextA.SetValue(inputText);

            Assert.AreEqual(inputText, vpTextA.Current.Value);
        }

        /// <summary>
        /// テストメソッド
        /// テキストボックスBに"987654321"を入力し正しいか
        /// </summary>
        [Test]
        public void TestTextBoxB()
        {
            var inputText = "987654321";

            // テキストボックスAに入力し入力が正しいか
            var inputB = findElementById(aeWindow, "txtB");
            var vpTextB = inputB.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            vpTextB.SetValue(inputText);

            Assert.AreEqual(inputText, vpTextB.Current.Value);
        }

        /// <summary>
        /// テストメソッド
        /// 計算結果が正しく表示されるか
        /// </summary>
        [Test]
        public void TestMethod()
        {
            // テキストボックスに"1234"を入力
            var inputA = findElementById(aeWindow, "txtA");
            var vpTextA = inputA.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            vpTextA.SetValue("1234");

            // テキストボックスに"5678"を入力
            var inputB = findElementById(aeWindow, "txtB");
            var vpTextB = inputB.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            vpTextB.SetValue("5678");

            // コンボボックスを"-"に選択
            var inuptOpe = findElementById(aeWindow, "cmbOpe");

            // コンボボックスを開く
            var ecpOpe = inuptOpe.GetCurrentPattern(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern;
            ecpOpe.Expand();

            // コンボボックスの項目を取得
            var itemsOpe = inuptOpe.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.ListItem));

            // "-"の項目を取得
            var setMinus = itemsOpe.Cast<AutomationElement>().First(w => "-".Equals(w.Current.Name));

            // "-"を選択
            var sipOpe = setMinus.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern;
            sipOpe.Select();

            // コンボボックスを閉じる
            ecpOpe.Collapse();

            // ボタン
            var button = findElementById(aeWindow, "button");
            var ipButton = (InvokePattern)button.GetCurrentPattern(InvokePattern.Pattern);
            ipButton.Invoke();

            // テキストボックス(解答)
            var inputAns = findElementById(aeWindow, "txtAns");
            var vpTextAns = inputAns.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
            var ans = vpTextAns.Current.Value;

            // テスト結果
            Assert.AreEqual("-4444", ans);
        }

        /// <summary>
        /// UI Automation要素からIDを指定してUI Automationを取得する
        /// </summary>
        /// <param name="element">UI Automation</param>
        /// <param name="automationId">ID</param>
        /// <param name="treeScope">スコープ</param>
        /// <returns></returns>
        private static AutomationElement findElementById(
            AutomationElement element,
            string automationId,
            TreeScope treeScope = TreeScope.Element | TreeScope.Descendants)
        {
            var target = element.FindFirst(
                            treeScope,
                            new PropertyCondition(
                                    AutomationElement.AutomationIdProperty,
                                    automationId));
            return target;
        }
    }
}

テスト実行

VisualStudioの「テスト」-「実行」-「すべてのテスト」でテストを実行します。
実行するとテスト対象アプリケーションが起動し、コードで記述した手順でUIが操作されます。
今回は、3回アプリケーションが起動しテストが実行されます。実行結果は下記のようになります。

UIAutomation_06
UIAutomation_07

さいごに

簡単なアプリケーションのUIのテストをコードで実行が行えることができました。
UI Automationですべてのテストを行うことは難しく、どの範囲でテストを行うか決めておくことをおすすめします。
さらにUI Automationで対応していないコントロールもあるため、
開発段階で使用できるコントロールを制限するなど考えてください。
今回は、自分で作成したアプリケーションが対象でしたが、
spy++などを使用することによりアプリケーションの構造を調査することも可能になります。

最後までご高覧頂きまして有難うございます。