読者です 読者をやめる 読者になる 読者になる

ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Friendly ハンズオン8 Friendly.Windows.Grasp - .Netアプリ操作でよく使う機能 -

Friendly(Win32, WinForms, WPF)

前回でFriendly.Window.Graspがどのようなものか概略を説明しました。
まだちょっとボヤっとしてますよねw?
ここでは、実際に使ってみてもう少し理解を深めてもらおうと思います。

ハンズオンの練習用プロジェクト

練習用のプロジェクトはこちらからダウンロードお願いします。
Ishikawa-Tatsuya/HandsOn8 · GitHub
ダウンロードが終わったらビルドしてから対象アプリを起動してみてください。

操作対象アプリ

「モーダレスボタン」を押すと、モーダレス画面が3枚表示されます。
f:id:ishikawa-tatsuya:20150105004822p:plain
「モーダルボタン」を押すとモーダルダイアログが1枚表示されます。
f:id:ishikawa-tatsuya:20150105005221p:plain
「モーダル連続ボタン」を押すとモーダルダイアログが表示され、そこでOKを押すともう一回モーダルダイアログが表示されます。(キャンセル時は表示されません)
f:id:ishikawa-tatsuya:20150105005227p:plain

テストプロジェクト

テストプロジェクトも雛形を作っています。参照の設定とusingまでやっていて、すぐにWindowControlの使い方を練習できるようになっています。

ハンズオン開始

モーダレスウィンドウの特定

Windowsアプリは複数のトップレベルウィンドウを同時に表示することが可能です。そのためモーダレスの操作はテストに欠かせない要素となります。これに関しては特定さえできれば、後はそれほど難しくありません。
では練習してみます。操作内容は
①「モーダレスボタン」を押す
②Windowタイトルが「1」のWindowを捕まえる
③背景色を変更

[TestMethod]
public void TopLevelWindowTest()
{
    var buttonModaless = new FormsButton(_form._buttonModaless);
    buttonModaless.EmulateClick();
         
    //1のウィンドウを見つける
    var form1 = WindowControl.WaitForIdentifyFromWindowText(_app, "1");

    //背景色を変えてみる
    form1.Dynamic().BackColor = Color.Red;
    Assert.AreEqual(Color.Red, (Color)form1.Dynamic().BackColor);
}

最後の行にブレイクを設定してデバッグ実行してみましょう。目的のウィンドウが赤に変わっていますね。
f:id:ishikawa-tatsuya:20150105212639p:plain
WaitForIdentifyFromWindowTextを使って、目的のウィンドウが確実に表示されるのを待っています。実はこのケースではIdentifyFromWindowTextでも問題なく動作します。(WaitForで待たなくても)と言うのは、その前の

buttonModaless.EmulateClick();

を抜けた時点で次のフォーム3枚の初期化が完全に完了して取得可能な状態になっているからです。同期処理なのでEmulateClickが完了した時点で相手プロセスでクリックイベントに紐づく処理は全て完了しています。相手プロセスがよほどトリッキーな処理をしていなければ問題ありません。ただ、たまにトリッキーな実装に変更される場合があるので、WaitForの方を使っておく方が無難です。

もう一つポイントがあります。form1に対してDynamic()を使っていますね。WindowControlがラップしたウィンドウハンドルに対応するWindowが.Netのオブジェクトの場合はそれを使えます。この場合のform1はSystem.Windows.Forms.Formなので使えるのですね。

モーダルダイアログ対応 -基本-

Windowsアプリ操作の最重要ポイントの一つと言えるでしょう。モーダルダイアログ対応です。モーダルダイアログ対応は難易度が上がります。非同期処理が必要だからです。今までのGUI操作は全て同期処理でした。そのため、安心してシーケンシャルに処理を実行できたのですね。
あと、モーダルダイアログはかなり特徴的な動きをします。詳細は長くなるのでまた別の機会にしますが、まあ色々ハマりポイントがあるのです。
でも安心してください。WindowControlを使えば、そんなモーダルダイアログへの対応もバッチリです!
では、やってみます。
操作手順は以下のものです。
①「モーダルボタン」を押す
②モーダルダイアログを捕まえ、「Cancelボタン」を押す

[TestMethod]
public void ModalTest()
{
    var mainForm = new WindowControl(_form);
    var buttonModal = new FormsButton(_form._buttonModal);

    //モーダルボタンを非同期で押す
    var async = new Async();
    buttonModal.EmulateClick(async);

    //モーダルダイアログが表示されるのを確実に待ち合わせる
    var dlg = mainForm.WaitForNextModal();

    //キャンセルボタンを押す
    var buttonCancel = new FormsButton(dlg.Dynamic()._buttonCancel);
    buttonCancel.EmulateClick();

    //非同期で実行したモーダルボタン押下の処理が完全に終了するのを待つ
    async.WaitForCompletion();
}

ポイントはAsyncの使い方とWaitForNextModalです。
図に書いてみます。
f:id:ishikawa-tatsuya:20150105221543p:plain

非同期で実行させる

モーダルダイアログはイベント処理をせき止めます。そのため、同期実行ではテストシナリオが固まってしまうのですね。そのために非同期で実行します。何度か書きましたが、対象アプリとテストシナリオはプロセスが異なるので当然スレッドも異なります。つまりそれぞれ、独立して非同期で動作するものなのです。しかし、非同期の制御では確実に動作するテストを書くのが非常に困難なので、Friendlyでは一つ一つの処理が確実の終わるまで待つように同期制御にしているのです。しかし、モーダルダイアログだけは、同期実行では問題があるのでAsyncクラスを使って非同期実行にしているのです。

と言うことは、ここは難易度が上がっています。気を付けないと、タイミング依存で失敗する可能性があるのです。通常のプログラミングでも同じですが、マルチスレッドのタイミング依存の失敗と言うのは非常に厄介なバグとなるのです。

待ち合わせは何のため?

非同期にするのは分かりました。では、待ち合わせるのはなぜでしょう?
それは、先にあげたタイミング依存での失敗をなくすためです。
実は、モーダルの表示と終了、書き方によってはその後の処理に、割り込む余地があります。(詳細は長くなるのでまた別の機会にやります。)つまり意図せぬタイミングで次の命令を相手プロセスに送ってしまう可能性があるのですね。これがタイミング依存での失敗の原因です。そのため、確実に表示されるまで待って、かつ最初に送った「モーダルボタン」押下処理が完全に終了するまで待つのです。

ちなみに

当然のことながら、相手プロセス側で非同期にしたり、タイマ処理やPostMessageで遅延処理を実行している場合はさらに隙ができています。これは相手プロセスの内部実装の情報がないとどうにもなりません。この辺の情報は開発側とシェアして必要に応じて独自の待ち合わせ処理を追加する必要があります。その辺りの手法に関しては後のアプリケーションドライバに関するハンズオンで説明します。

モーダルダイアログ対応 -連続-

次は連続モーダルダイアログ対応です。
これは一つのイベント、例えば「ボタンを押す」のイベントハンドラの中にダイアログを表示するコードが二つ続けて書かれている場合ですね。このパターンは結構ありますよね。

[TestMethod]
public void ModalCatenaTest()
{
    var mainForm = new WindowControl(_form);
    var buttonModalCatena = new FormsButton(_form._buttonModalCatena);

    //モーダル連続ボタンを非同期で押す
    var async = new Async();
    buttonModalCatena.EmulateClick(async);

    for (int i = 0; i < 2; i++)
    {
        //モーダルダイアログが表示されるのを確実に待ち合わせる
        var dlg = mainForm.WaitForNextModal();

        //OKボタンを押す
        var buttonOK = new FormsButton(dlg.Dynamic()._buttonOK);
        buttonOK.EmulateClick();

        //ダイアログが完全に消滅するまで待つ
        dlg.WaitForDestroy();
    }

    //非同期で実行したモーダル連続ボタン押下の処理が完全に終了するのを待つ
    async.WaitForCompletion();
}

ポイントはWaitForDestroyですね。

 dlg.WaitForDestroy();

これを入れておかないと、稀に二回目のWaitForNextModalで前の消えかけのダイアログを取得する場合があるのです。連続でダイアログが表示されるケースでは必ず入れてください。

他にも機能はありますが

一旦これだけでOKです。
他の機能はまた必要になれば紹介していきます。

次回は・・・

ここまでで、WinFormsのアプリはかなり操作できるようになったと思います。でも、あと少し知識が必要なのです。それはWin32のWindowの操作に関してです。
.Netアプリ操作なのに?
そうなんです。.Netアプリでも実はネイティブのWindowが出るケースがあるのです。そしてそれはほとんどのアプリで使われいるのです。
と言うことで次回はネイティブの話です。