より良いエンジニアを目指して

1日1つ。良くなる!上手くなる!

FlaUIをご紹介〜C# Qiita Advent Calender 2018 5日目

QiitaのAdvent Calenderに初投稿になります。

FlaUIとは

C#のAdvent Calenderということで、今回はFlaUIというライブラリを紹介させていただきます。

github.com

どういうものかというと、自動UIテストを行うためのライブラリです。

TestStack.Whiteとの比較

同様のライブラリとしては、TestStack.Whiteもあります。

TestStack.Whiteも使ったことがあるのですが、下記の理由で、このライブラリを使うことにしました。

DateTimePickerについてFlaUIでは対応していないくらいで、TestStack.Whiteに劣っていると感じるところはありません。

これについては、Keyboardクラスを用いて、入力するメソッドを自前で作成して対応しました。

使い方とかのドキュメントは?

英語ですが下記のWikiを見れば基本的なことは事足りるかなと思います。

Home · Roemer/FlaUI Wiki · GitHub

実践

全てを紹介することはできず、触りだけとなってしまいますが、実践してみます。

手元にあったこんなアプリをテストします。

f:id:rimever:20181201181424p:plain

1.「吾輩は猫である」の文章の一覧が、左側にリストボックスとして表示されている 1.リストボックスの文章をクリックすると、文章内の単語の関係が有向グラフで表示される 1. 右側の下部のテキストボックスには選択した文章が表示される

テスト計画

今回は、下記のような自動操作をしてみます。

  1. リストボックスの文章をクリック
  2. クリックした文章が右下のテキストボックスにも表示されているかアサート
  3. 有向グラフについてはアサートしようがないので、スクリーンショットを出力して後で確認することにする。

プロジェクト作成

テストプロジェクトを作成し、NuGetでFlaUIを入手します。

f:id:rimever:20181201201256p:plain

が、 FlaUI.UIA3とかFlaUI.UIA2とか出てきてしまいます。UIA2かUIA3のどちらのライブラリを使うか判断する必要があります。

詳細は、公式サイトを参照していただくとして、簡単にいうと

  • FlaUI.UIA2 … WinForms
  • FlaUI.UIA3 … WPF,Windows Store Apps

です。

今回は、WPFアプリケーションですのでFlaUI.UIA3をインストールします。

起動テスト

まずは、小手調べに、起動して、自動的にアプリを閉じるというプログラムを書いてみます。

#region

using System;
using System.Threading;
using FlaUI.Core;
using FlaUI.UIA3;
using Xunit;

#endregion

namespace Chapter05.Q44.Tests
{
    public class AutomationTest
    {
        private readonly string targetApplicationPath = "Chapter05.Q44.exe";

        /// <summary>
        /// アプリの起動をテストします。
        /// </summary>
        [Fact]
        public void LaunchTest()
        {
            var app = Application.Launch(targetApplicationPath);
            try
            {
                using (var automation = new UIA3Automation())
                {
                    app.GetMainWindow(automation);
                    Thread.Sleep(1000); // すぐ閉じてしまうので、ここで1秒待つ
                }
            }
            finally
            {
                app.Close();
            }
        }
    }
}

今回はXUnitも合わせてインストールして、テストメソッドとして提供していきます。

そうしておけば、メソッド単体を動かすことで、各テストケースを試すことができるためです。

  1. アプリが起動する
  2. 1秒待つ
  3. アプリが終了する

という一連の流れが確認できました。

f:id:rimever:20181201203700p:plain

アプリを終了する処理を入れておかないとアプリは起動されっぱなしになってしまうので、私はfinally句でアプリの終了を行うようにしています。

リストボックスをクリックする

メソッドを追加して、リストボックスをクリックする処理を作成します。

で、そのリストボックスをいかに取得するかが問題です。

いくつか取得方法があるのですが、今回はByAutomationIdで取得しようと思います。

        <ListBox x:Name="ListBoxSentence" Grid.Column = "0" SelectionChanged="ListBoxSentence_OnSelectionChanged"/>

xamlで上記のようにListBoxSentenceと宣言されていますので、これを指定して取得します。

今回はxamlを覗きましたが、それ以外の方法としては、Inspectツールを用いる方法です。

ツールを用いることで、コントロールのAutomationIdなどを調べることが可能です。

UISpyもありますが、これについてもFlaUIInspectというツールがありますので、こちらを用いると良いかと思います。

        /// <summary>
        /// リストボックスの選択をテストします。
        /// </summary>
        [Fact]
        public void SelectListBoxTest()
        {
            var app = Application.Launch(targetApplicationPath);
            try
            {
                using (var automation = new UIA3Automation())
                {
                    var window = app.GetMainWindow(automation);
                    var automationElement = window.FindFirstDescendant(factory => factory.ByAutomationId("ListBoxSentence"));
                    Assert.NotNull(automationElement);
                    var listBox = automationElement.AsListBox();
                    listBox.Select(3);
                    Thread.Sleep(1000);
                }
            }
            finally
            {
                app.Close();
            }
        }

f:id:rimever:20181201204856p:plain

indexが3の項目(上から4番目)を選択したことが確認できました。

                    var automationElement = window.FindFirstDescendant(factory => factory.ByAutomationId("ListBoxSentence"));
                    Assert.NotNull(automationElement);
                    var listBox = automationElement.AsListBox();
                    listBox.Select(3);

先ほどのソースから抜粋しています。

  1. ListBoxSentenceがAutomationIdのコントロールを取得します。
  2. 取得できているか確認するためにNotNullでアサート(元のプログラムが変更していないかチェックするため)
  3. 取得しただけではAutomationElementなのでListBoxとして取得。
  4. indexが3の項目を選択。

テキストボックスの値をチェックする

テキストボックスの値がリストボックスの選択した内容と同じ値であることをチェックします。

また、テキストボックスの値をコンソール出力してみます。

        /// <summary>
        /// テキストボックスに値がセットされたことをテストします。
        /// </summary>
        [Fact]
        public void SetTextBoxTest()
        {
            var app = Application.Launch(targetApplicationPath);
            try
            {
                using (var automation = new UIA3Automation())
                {
                    var window = app.GetMainWindow(automation);
                    var listBox = window.FindFirstDescendant(factory => factory.ByAutomationId("ListBoxSentence"))
                        ?.AsListBox();
                    Assert.NotNull(listBox);
                    listBox.Select(3);
                    var textBox = window.FindFirstDescendant(factory => factory.ByAutomationId("TextBoxSentence"))
                        ?.AsTextBox();
                    Assert.NotNull(textBox);
                    Assert.Equal(listBox.SelectedItem.Text, textBox.Text);
                    _output.WriteLine(textBox.Text);
                    Thread.Sleep(1000);
                }
            }
            finally
            {
                app.Close();
            }
        }

f:id:rimever:20181201213239p:plain

問題なく、コントロールの値がコンソール出力されることが確認できました。

※XUnitはConsole.WriteLineではコンソール出力できないため、対応が必要です。

スクリーンショットを取る

        /// <summary>
        /// リストボックスの選択
        /// テキストボックスの項目確認
        /// 有向グラフのチェックのために画像を出力します。
        /// </summary>
        [Fact]
        public void SaveScreenShot()
        {
            var app = Application.Launch(targetApplicationPath);
            try
            {
                using (var automation = new UIA3Automation())
                {
                    var window = app.GetMainWindow(automation);
                    var listBox = window.FindFirstDescendant(factory => factory.ByAutomationId("ListBoxSentence"))
                        ?.AsListBox();
                    Assert.NotNull(listBox);
                    listBox.Select(3);
                    var textBox = window.FindFirstDescendant(factory => factory.ByAutomationId("TextBoxSentence"))
                        ?.AsTextBox();
                    Assert.NotNull(textBox);
                    Assert.Equal(listBox.SelectedItem.Text, textBox.Text);
                    Capture.Element(window)
                        .ToFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test.png"));
                    Thread.Sleep(1000);
                }
            }
            finally
            {
                app.Close();
            }
        }

f:id:rimever:20181201214648p:plain

いけましたね。

                    Capture.Element(window)
                        .ToFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test.png"));

公式ではCapture.Screen()が紹介されていますが、お手軽とはいえ、スクリーン全体のキャプチャなので、ウインドウ以外もスクリーンショットになってしまいます。

ですので、Capture.Elementを用いる方がオススメです。

どうしても、目視せざるを得ないケースはあります。

そうした時は画像出力が頼みの綱となります。

肩身が狭いデスクトップアプリケーションだとしても、今時のスナップショットテストをやりたいですよね!

使ってみて

ほっておけば、テストしてくれて、エビデンスも生成してくれる。

Excelスクショ職人にならずに済む、これこそ働き方改革

と言いたいところですが、TestStack.Whiteもそうですが、このライブラリを使えば、全ての操作をテストできるとまでは言えるものではありません。

私が使いこなせていないのか、たまにクリックできるのに例外が発生したりします。

クリックしたら、コンテキストメニューが出るはずなのに、別の環境で動かしたら、表示される前にコンテキストメニューを取得したせいか、例外発生ということがありました。

これに対しては、Retryクラスを用いて、取得を再試行するようにして対応しました。

複雑な操作となると手が届かないですし、アプリが改修される度に、自動UIテストのメンテナンスを行うコストも高くつくことは事実です。

やりすぎないことも大事になってきます。

それでも、アプリが確実に動いているという安心感を得やすくなったなと思います。   私は心配性なので、設定を変更して、正しく動かなくなったら、どうしようと思ったりします。

とりあえず、これを動かしてみて一通りの動作確認して安心できるようになりました。

Advent Calenderを書いてみて

C#はデスクトップアプリではなく、 UnityやWEBやXamarinなど幅広く使われる言語ですので、デスクトップアプリの自動UIテストというマニアックな題材で刺さらなかった方には申し訳ないです。

本当は、Microsoft Tech Summitの3日目、最後のトリを飾る牛尾剛さんのMicrosoft Azureについての、Durable Functionに関するセッションを受けて、Durable Functionについて書くプランもありました。

僕も「Durable!」とか叫んでみたい。とか、これで筋肉で解決できないことも解決できる!とかネタを考えていたのですが、肝心のDurable Functionで実際、何かしようというアイデアが浮かびません。

来年に持ち越しです。

qiita.com