【UIA】UI Automation を用いたユーザーインターフェイスのテスト - 4 (Final)

■■■
2010.09.27 追記:
Visual Studio 2010 Premium, Ultimate にて 自動 UI テストが新機能として搭載されました。こちらを使うと、UI Automation に対応したアプリケーションの UI テストを、単体テストフレームワークとして実施可能です。テストしたい UI 動作のレコーディングおよび、検証(アサーション)の設定を GUI で行い、単体テストコードを自動生成してくれます。
参考:
https://blogs.msdn.com/b/tomohn/archive/2010/09/16/essense-of-tfs-vol-15-ui-automation-testing-with-team-foundation-server-2010.aspx
■■■

前回までで、WPF アプリケーションの UI をテストするための単体テストを記述する環境が整いました。今回は、いよいよ単体テストコードの一つの例を示したいと思います。

実施すべきことは、大きく分けて3つになります。

  1. アプリケーションを起動し、各 UI の情報を取得する
  2. UI 操作を行い、検証を行う
  3. アプリケーションを終了する

UI Automation では、検証対象のアプリケーション(UI オートメーション プロバイダ)が UI の情報を公開していれば、検証するテストコード(UI オートメーション クライアント)からその情報を利用することができます。それを利用することで上述の3つをテストコードとして実装できるのです。

※ここでは、単体テスト用のクラス、テストメソッド、テスト属性について触れていますが、これらの詳細な紹介/説明は省略します(このあたりをまず知りたいかたはこのブログの単体テスト関連の投稿などをご覧ください)。

アプリケーションを起動し、各 UI の情報を取得する

まずは、テストクラスにフィールドを追加しておきましょう(※別にテストメソッド内にすべて書いてもいいですが、冗長になりますので効果的に)。

[TestClass()]
public class Window1Test {
private TestContext testContextInstance;
// for UIA private Window1 window;
private WindowAutomationPeer winPeer;
private ButtonAutomationPeer buttonPeer;
private TextBoxAutomationPeer textBoxPeer;
private LabelAutomationPeer labelPeer;
/* 以下、省略 */ }

ここで注目してほしいのが、// for UIA 以下の部分です。Window1 というのは、テスト対象アプリケーション自体ですので、まぁいいとして(名前はアプリによって異なるということです)、その下に、4つ XxxxAutomationPeer というのがあります。

AutomationPeer とは、ボタンやテキストボックスといったコントロール(型)を UI オートメーション用に公開してくれるもので、言ってみれば、各 UI を AutomationPeer を介して操作すると思っていただければいいかと思います。アプリケーションの公開されている UI 情報にアクセスするためのインターフェイスというかリモコンというかそんな感じのイメージですか。

これを操作したい UI 分用意する必要があります(不要なものはもちろん、要りませんし、テストにより、使う UI は異なるので、フィールドとして定義するだけではなく、各テストメソッド内で定義してもいいでしょう)。

この時点では、AutomationPeer を宣言したにすぎませんので、これで UI を操作できるようになったわけではもちろんありません。そこで、

// ウィンドウを生成・表示 window = new Window1();
window.Show();

// 各部品の情報を設定 winPeer = new WindowAutomationPeer(window);
List<AutomationPeer> children = winPeer.GetChildren();

とすると、アプリケーションを起動し、その画面を操作するための winPeer というのを生成、さらにこの画面に含まれる UI (Children)を取得することができます。

これで、アプリケーションの画面の情報を入手することができました。でもまだやることがあります。UI の情報を入手できましたが、このままでは、プログラムコードで操作するのには、不便です。なぜなら、UI 要素の特製まで取得できていません。具体的な UI の情報を入手し、利用するまでの準備には至っていないことになるのです。そこで、上述のコードの続きとして、

for (var i = 0; i < children.Count; i++)
{
string name = children[i].GetName();
switch (name)
{
case "xInputTextBox":
textBoxPeer = (TextBoxAutomationPeer)children[i];
break;
case "xPerformButton":
/* 以下、省略 */ }
}

といった感じ(実装方法は、もちろんいろいろあります)で、各 UI を識別し、宣言しておいた各 AutometionPeer にあてはめてあげると、あとで UI を操作しやすくなります。

ここで、注目していただきたいポイントは、UI の名前で識別しているところです。children[i].GetName() というところです。GetName() で AutomationProperties.Name の情報を取得できるのです。したがって、アプリケーション側が Accessibility(ユーザー補助) のベストプラクティスに基づき、UI の情報を公開してくれていれば、それをもとに各 UI を識別し、各 UI の専門特化した、AutomationPeer を利用することができるのです(アプリケーション側のどこで設定していたかは第2回をご覧ください)。

UI の情報が公開されていなかったとしても、ID の情報などを知っていれば、それを識別子として UI を特定することはできます。しかし、デザインにより情報が変化する場合には、やはり一意に特定できる拠り所があった方がいいに決まっています。

さて、この上述の一連のコードは、どこに記述するとよいでしょうか?テストメソッドの中に全部記述してもいいでしょう。でも冗長ですよね?なので、 [TestInitialize()] で行うとよいでしょう。これなら複数のテストメソッドがあったときにも、テストの前にここで記述した処理を行ってくれます。要するにテストしたい UI 操作のお膳立てをやってくれることになります。全容を書くと以下のようになります:

[TestInitialize()]
public void MyTestInitialize()
{

    // ウィンドウを生成・表示 window = new Window1();
window.Show();
   

// 各部品の情報を設定 winPeer = new WindowAutomationPeer(window);
List<AutomationPeer> children = winPeer.GetChildren();

    for (var i = 0; i < children.Count; i++)
{
string name = children[i].GetName();
switch (name)
{
case "xInputTextBox":
textBoxPeer = (TextBoxAutomationPeer)children[i];
break;
case "xPerformButton":
/* 以下、省略 */ }
}

UI 操作を行い、検証を行う

ここまでお膳立てができていれば、今すぐにでも UI を操作できます。あとはテストメソッドで、各 AutomationPeer を通じて操作すればいいわけです。

たとえば、テキストボックスに値をセットしたければ・・・

((IValueProvider)textBoxPeer).SetValue("ながさわともはる");

なんてことができるし、

Button button = (Button)(buttonPeer.Owner);
RoutedEventArgs args = new RoutedEventArgs(Button.ClickEvent, button); button.RaiseEvent(args);

といった感じで、ボタンをクリックしてイベントを発生されることもできます。

したがって、テストメソッドとしてはたとえば、以下のようなものが記述できるのです:

[TestMethod()]
public void PerformButtonクリックテスト()
{
// テキストボックスへの入力テストを実行 string textString = "Open XML が!";
// テキストボックスに文字列を設定 ((IValueProvider)textBoxPeer).SetValue(textString);

    // テキストボックスの文字列設定の検証 Assert.AreEqual(textString, ((IValueProvider)textBoxPeer).Value, "InputTextBoxの値設定で失敗");

    // ボタン押下によるイベントを発生 Button button
= (Button)(buttonPeer.Owner);

    RoutedEventArgs args
= new RoutedEventArgs(Button.ClickEvent, button);

    button.RaiseEvent(args);

    Label label
= (Label)(labelPeer.Owner);
string actual
= label.Content.ToString();

    // イベント発生後の結果の検証 Assert.IsFalse(System.String.IsNullOrEmpty(actual), "処理が正しく実行されていないため失敗");

    Assert.AreEqual(textString, actual, "文字列が一致していないため失敗");
}

書き方は、いろいろあると思います。もっとよい書き方もきっとあるんだろうなと思いながら書いてます。「こっちの方がいいよ!」というものがありましたら、ぜひフィードバックいただければ!

ちなみに、参考にしたのは、https://miketwo.blogspot.com/2007/03/unit-testing-wpf-controls-with.html です。

アプリケーションを終了する

さて、テストが終わったら、起動したアプリケーションは閉じなければいけません。お膳立てを [TestInitialize()] で行ったならば、締めは、 [TestCleanup()] で行うのがよいでしょう。そこで以下のように記述しておきます:

[TestCleanup()]
public void MyTestCleanup()
{
// ウィンドウの終了 window.Close();

    System.Windows.Threading.Dispatcher.CurrentDispatcher.InvokeShutdown();
}

普通に考えると、window.Close(); だけでいいのですが・・・これだとテスト時に以下のエラーとなってしまいます。

バックグラウンドのスレッドの 1 つが例外をスローしました: System.Runtime.InteropServices.InvalidComObjectException: 基になる RCW から分割された COM オブジェクトを使うことはできません。
場所 System.Windows.Input.TextServicesContext.StopTransitoryExtension()
場所 System.Windows.Input.TextServicesContext.Uninitialize(Boolean appDomainShutdown)
場所 System.Windows.Input.TextServicesContext.OnAppDomainUnloaded(Object sender, EventArgs args)

こうならないために、System.Windows.Threading.Dispatcher.CurrentDispatcher.InvokeShutdown(); を記述しておいてください(Visual Studio 2008 での問題です)。

このあたりについては、

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=318333

をご覧ください。

ちなみに、UI Automation を用いる際には、STA で動かす必要があります。Visual Studio 2008 の単体テスト フレームワークは、STA が既定ですから、このあたりは気にしなくても大丈夫です。
(気になる方は、関連投稿 をご覧ください)

さてさて、最後に UI 情報を取得するのに便利なツールをご紹介しましょう。それは、UISpy です。UISpy は Windows SDK に含まれるツールですが、これを用いると起動しているアプリケーションの UI 情報を取得したり、確認したりすることができます(詳細は、https://msdn2.microsoft.com/ja-jp/library/ms727247(VS.80).aspx をご覧ください)。

image
(クリックすると拡大表示されます)

上記スクリーンショットをご覧いただければ、AutomationProperties で設定した UI 情報が表示されているのがわかります。

関連する情報として以下の2つをあげさせていただきます:

 

ながさわともはる