Quantcast
Channel: ささいなことですが。
Viewing all 104 articles
Browse latest View live

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

$
0
0

前回で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]
publicvoid 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]
publicvoid 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]
publicvoid 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が出るケースがあるのです。そしてそれはほとんどのアプリで使われいるのです。
と言うことで次回はネイティブの話です。


Friendlyハンズオン9 Win32用上位ライブラリ

$
0
0

今回はWin32の話です。
プロダクトによっては、過去の資産を持っていて、一部ネイティブで作っているものもありますよね。
そうでなくて完全な.Netのアプリでも、実はネイティブの画面は出てくるのです。
と言うことで、ネイティブの画面も操作できるようにしておく必要がありますね。

FriendlyはネイティブのDLL公開関数も実行させることができます。
ネイティブDLL公開関数の呼び出し - 株式会社Codeer (コーディア)
ただ、.Netに比べてかなり面倒です。
なので、一般的なコントロール操作に関してはラップしたライブラリを用意しています。
Friendly.Windows.NativeStandardControlsです。これをご利用ください。
APIリファレンス
ソースコード

対応表

WindowクラスFriendly.Windows.NativeStandardControls
ButtonNativeButton
ComboBox、ComboBoxEx32NativeComboBox
SysDateTimePick32NativeDateTimePicker
Edit、RichEdit20A、RichEdit20WNativeEdit
IPAddress32NativeIPAddress
ListBoxNativeListBox
SysListView32NativeListControl
SysMonthCal32NativeMonthCalendar
msctls_progress32NativeProgress
ScrollBarNativeScrollBar
msctls_trackbar32NativeSlider
msctls_updown32NativeSpinButton
SysTabControl32NativeTab
SysTreeView32NativeTree

特殊なものとして、メッセージボックスに対応したクラスもあります。

特殊Friendly.Windows.NativeStandardControls
メッセージボックスNativeMessageBox

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

こちらからダウンロードしてください。ブロック解除もお願いします。
https://github.com/Ishikawa-Tatsuya/HandsOn9
対象アプリはMFCで作っています。
ビルドできない人もいるかもしれないので、ソースコードも入れていますが、exeも作っています。

対象はMFCApplication.exeです。
ListBoxとTreeViewがあります。
f:id:ishikawa-tatsuya:20150107230118p:plain

OKボタンを押すと、メッセージボックス(モーダル)が表示されます。
f:id:ishikawa-tatsuya:20150107230430p:plain

Resource.hには次のように定義されています。
ListBoxとTreeViewにそれぞれ割り当たっているダイアログIDです。あ、OKボタンにはIDOKが割り当たっています。

#define IDC_LIST                        1000#define IDC_TREE                        1001

テストプロジェクトはいつものように参照設定とusingまで終わっています。
自分のテストに組み込む場合はNugetからFriendly.Windows.NativeStandardControlsを取得してください。

ハンズオン開始

ListBox

まずはリストボックスです。
選択を変えてみましょう。

[TestMethod]
publicvoid TestListBox() 
{
    var listBox = new NativeListBox(_dlg.IdentifyFromDialogId(IDC_LIST));
    listBox.EmulateChangeCurrentSelectedIndex(3);
    Assert.AreEqual(3, listBox.CurrentSelectedIndex);
}

.Netの場合はラッパークラスのコンストラクタの引数に対象アプリ内のコントロールのオブジェクトを渡していました。しかし、ネイティブの場合はそれがありません。渡すものは次のいずれかです。

  • Windowハンドル
  • WindowControl

今回の場合はWindowControlを渡しています。
それはIdentifyFromDialogIdを使って取得しています。ネイティブの場合はダイアログIDから取得するのが一番いいですね。これなら対象アプリで変えないようにコントロールしやすいです。
特定すると、後はWinFormsのものと同じような感覚で使えます。

TreeView

次はTreeViewです。
ノードの文字列を編集してみます。

[TestMethod]
publicvoid TestTreeView()
{
    var treeView = new NativeTree(_dlg.IdentifyFromDialogId(IDC_TREE));
    var node = treeView.FindNode("0", "2");
    treeView.EmulateEdit(node, "x");
    Assert.AreEqual("x", treeView.GetItemText(node));
}

これも特定方法は同じですね。
NativeTreeのインターフェイスはネイティブのTreeViewのインターフェイスを少し意識した感じになっています。ノードはIntPtrで返ってきます。

メッセージボックス

少し特殊です。
コントロールではなくメッセージボックスをラップしたものです。

[TestMethod]
publicvoid TestMessageBox()
{
    //OKボタンを非同期で押す
    var buttonOK = new NativeButton(_dlg.IdentifyFromDialogId(IDOK));
    var async = new Async();
    buttonOK.EmulateClick(async);

    //メッセージボックスを取得してラップ
    var msg = new NativeMessageBox(_dlg.WaitForNextModal());
    
    //メッセージを取得
    Assert.AreEqual("Msg", msg.Message);
    
    //テキストからボタンを検索して押す
    msg.EmulateButtonClick("OK");

    //非同期処理の完了待ち
    async.WaitForCompletion();
}

メッセージボックスはほとんどのアプリで使います。なので、このクラスは頻繁に使うと思います。

ラッパーを使う分にはネイティブでもさほど変わりませんね

特定方法は異なりますが、ラッパーを使うとネイティブのアプリでも割と簡単に操作できます。
でも、本気でWin32のアプリを自動化するには恐らくこれだけでは足りなくて、独自に定義したDLL公開関数を呼び出したりも必要になってくると思います。その辺もおいおいやっていきますが一旦.Netアプリ操作をメインで解説していきます。

次回は「.Netアプリで出てくるネイティブウィンドウ対応」のハンズオンです。

Friendly ハンズオン 10 .Netアプリで出てくるネイティブウィンドウ対応

$
0
0

前回Win32用の上位ライブラリのNativeStandardControlsを使う練習でした。
今回は、さらに実践的に.Netで出てくるWin32のWindowに対応してみます。
先にネタをバラしておきます。
操作方法は書きますが、最終的にはメッセージボックス以外は操作しない方がいいです。
???じゃあどうするかって?
それは、最後まで読んでみてくださいw

.Netなのに、出てくるWin32のウィンドウって何?

よく使う機能で3つあります。

・メッセージボックス
f:id:ishikawa-tatsuya:20150108224628p:plain
・ファイルダイアログ
f:id:ishikawa-tatsuya:20150108224635p:plain
・フォルダダイアログ
f:id:ishikawa-tatsuya:20150108224650p:plain
これらはWinFormsだけでなくWPFでも同様です。
使うときは、.Netのクラスが用意されていますが、あれはネイティブの処理をラップしているだけで、出てくるダイアログやその実装はネイティブなのですね。

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

こちらからダウンロードお願いします。
Ishikawa-Tatsuya/HandsOn10 · GitHub
ダウンロードしたらビルドして、起動してみてください。
f:id:ishikawa-tatsuya:20150108225006p:plain
それぞれのボタンを押すと上記のダイアログが出ます。
そして、ファイルダイアログとフォルダダイアログは、それぞれ選択したパスがタイトルに設定されます。
f:id:ishikawa-tatsuya:20150108225400p:plain

テストプロジェクトも参照と最初の画面のボタン取得まで終わっています。このコードに書き足していってください。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using System.Diagnostics;
using System.Windows.Forms;
using Ong.Friendly.FormsStandardControls;
using Codeer.Friendly.Windows.Grasp;
using Codeer.Friendly.Windows.NativeStandardControls;
using System.Runtime.InteropServices;
using System.Linq;

namespace Test
{
    [TestClass]
    publicclass NetNativeWindowTest
    {
        WindowsAppFriend _app;
        WindowControl _form;
        FormsButton _buttonMessageBox;
        FormsButton _buttonFile;
        FormsButton _buttonFolder;

        [TestInitialize]
        publicvoid TestInitialize()
        {
            _app = new WindowsAppFriend(Process.Start("FormsTarget.exe"));
            _form = new WindowControl(_app.Type<Application>().OpenForms[0]);
            _buttonMessageBox = new FormsButton(_form.Dynamic()._buttonMessageBox);
            _buttonFile = new FormsButton(_form.Dynamic()._buttonFile);
            _buttonFolder = new FormsButton(_form.Dynamic()._buttonFolder);
        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            Process.GetProcessById(_app.ProcessId).CloseMainWindow();
        }
    }
}

ハンズオン開始

メッセージボックス

これは、前回もやりましたね。頻出すぎるので、ラッパークラスを用意しています。それを使ってください。

[TestMethod]
publicvoid TestMessageBox()
{
    var async = new Async();
    _buttonMessageBox.EmulateClick(async);

    //メッセージボックスを取得してラップ
    var msg = new NativeMessageBox(_form.WaitForNextModal());

    //メッセージを取得
    Assert.AreEqual("Msg", msg.Message);

    //テキストからボタンを検索して押す
    msg.EmulateButtonClick("OK");

    //非同期処理の完了待ち
    async.WaitForCompletion();
}

ファイルダイアログ

これは、面倒なコードになりますね。なのでファイルダイアログを操作するコードはOpenFileという関数にしました。
ファイルダイアログは画面要素の特定が非常に面倒ですね。「開くボタン」は文字列から取得できるとして、パスを入力するコンボボックスはどうしましょう?コンボボックスは二つありますね。座標で比較して下の方にしますか。

[TestMethod]
publicvoid TestOpenFileDialog()
{
    var async = new Async();
    _buttonFile.EmulateClick(async);

    string myPath = GetType().Assembly.Location;

    //ファイルを開く処理
    OpenFile(_form.WaitForNextModal(), myPath);

    //非同期処理の完了待ち
    async.WaitForCompletion();

    //フォームのテキストをチェック
    Assert.IsTrue(string.Compare(myPath, (string)_form.Dynamic().Text, true) == 0);
}

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
publicstaticexternbool GetWindowRect(IntPtr hwnd, out RECT lpRect);

staticvoid OpenFile(WindowControl fileDialog, string path)
{
    //ボタン取得
    var buttonOpen = new NativeButton(fileDialog.IdentifyFromWindowText("開く(&O)"));

    //コンボボックスは二つある場合がある。//下の方を採用。
    var comboBoxPathSrc = fileDialog.GetFromWindowClass("ComboBoxEx32").OrderBy(e =>
    {
        RECT rc;
        GetWindowRect(e.Handle, out rc);
        return rc.Top;
    }).Last();
    var comboBoxPath = new NativeComboBox(comboBoxPathSrc);

    //パスを設定
    comboBoxPath.EmulateChangeEditText(path);

    //開くボタンを押す
    buttonOpen.EmulateClick();
}

OpenFileは汎用的な関数にしましたので、もし操作する場合は、これを自分のプロジェクトにコピーして使ってください。

フォルダダイアログ

次はフォルダダイアログです。これもややこしいのでフォルダダイアログの操作はSelectFolderと言う関数にまとめてみました。

[TestMethod]
publicvoid TestFolderDialog()
{
    var async = new Async();
    _buttonFolder.EmulateClick(async);

    //フォルダ選択処理
    SelectFolder(_form.WaitForNextModal(), @"デスクトップ", @"PC", @"Windows (C:)", @"Program Files");

    //非同期処理の完了待ち
    async.WaitForCompletion();

    //フォームのテキストをチェック
    Assert.IsTrue(string.Compare(@"C:\Program Files", (string)_form.Dynamic().Text, true) == 0);
}

staticvoid SelectFolder(WindowControl folderDialog, paramsstring[] nodes)
{
    //ツリーは一つしかない
    NativeTree tree = new NativeTree(folderDialog.IdentifyFromWindowClass("SysTreeView32"));
    //OKというウィンドウテキストもひとつだけ
    NativeButton buttonOK = new NativeButton(folderDialog.IdentifyFromWindowText("OK"));

    //ツリーのノードを一つずつ取得して選択for (int i = 1; i <= nodes.Length; i++)
    {
        //フォルダツリーはフォルダを非同期に検索している(と思う)ので//期待のアイテムが表示されていない場合がある//表示されるまで待つ
        var itemPath = nodes.Take(i).ToArray();
        IntPtr item = IntPtr.Zero;
        while (item == IntPtr.Zero)
        {
            item = tree.FindNode(itemPath);
        }

        //展開と選択
        tree.EmulateExpand(item, true);
        tree.EmulateSelectItem(item);
    }

    //OKボタンを押す
    buttonOK.EmulateClick();
}

フォルダダイアログは画面要素の特定は何とかなりそうです。ツリーは一つしかないのでWindowクラスから特定できます。ボタンもWindowテキストから特定できますね。
でも、コードを見るとツリーのノード選択のコードが変ですね。何をやっているのでしょうか?
これは、ノードが表示されるのを待ち合わせているのです。
フォルダダイアログのツリーは展開した瞬間ではなく、若干遅延して子ノードを表示しています。ファイルアクセスなので仕方ないですね。なのでノードが取得可能な状態になるのを待っているのです。
SelectFolderも汎用的に作ってみたので、操作する場合はコピーして使ってください。

難しい?

はい。ファイルダイアログとフォルダダイアログはかなり難易度高いですね。しかも、これらはOSや個人設定によりデザインが変わってきます。そういうのは運用時のデメリットになってしまいます。
僕もメッセージボックスはラッパークラスを提供していますが、これらのダイアログは提供していません。何か問題発生しそうだからです。(各プロジェクトで作ってもらった場合は何とかなりますが、ライブラリとして公開して広く使われると、問題発生時に修正が困難なのです。)

何で難しい?

ネイティブだからでしょうか?
違います。
内部実装を知らないからです。
例えば、自分が作ったダイアログなら画面要素の取得もダイアログIDからできます。そして、今後そこが変わらないように調整できますし、変わったとしてもどう変えたか把握しているのでテストに反映も出来ます。内部実装だとしても運用的にはpublicで制御可能なものなのです。
しかし、他社(MSとか)の作ったものは、本当にpublicな仕様でないと、それで正しいのかわからないし、いつ変えられても文句は言えません。特にフォルダダイアログの例の「多分非同期で・・・」みたいなのは単なる推測です。
もし、これらを操作するとしても、最悪この辺りの微妙さを理解して、何か問題が発生したときの影響を最小限に抑えるような設計にする必要があります。
これは、Friendlyを使う場合に限らず自動化全般に言えることです。

*正確にはメッセージボックスも内部実装は知りません。しかし、難易度、使用頻度から考えて提供することにしました。

他人の作ったダイアログは無理に操作しなくてもいい。

結局ですね、
GUIの詳細をテストしたいのではない。結合したシステムをテストしたいのだ。」
と言うことなのですよ。
まあ、この例ではアレですけど、ホントのテストケースを作成する場合は、フォルダダイアログの振る舞いをテストしたいのではなくて、ダイアログでフォルダを選んだ先に実行される処理をテストしたいのですよね。フォルダダイアログは他人が作ったものです。まあテストする必要がないとは言いませんが、自動化して回帰検査で毎日テストしてやる価値はないですね。最後に人間が一回見れば済むことです。
と言うことで、対象アプリの「FolderBrowserDialogボタン」を押した処理のコードを見てみましょう。

privatevoid ButtonFolderClick(object sender, EventArgs e)
{
    //フォルダダイアログの操作は日々テストする程のものではない。string path = string.Empty;
    using (var dlg = new FolderBrowserDialog())
    {
        if (dlg.ShowDialog() != DialogResult.OK)
        {
            return;
        }
        path = dlg.SelectedPath;
    }

    //ここから先をテストしたい
    ExecuteFilePathCore(path);
}

privatevoid ExecuteFolderPathCore(string path)
{
    //本当はもっと難しい処理
    Text = path;
}

結局本来であれば、ExecuteFolderPathCore以降の処理がテストできればいいんですよね。なので、これを呼び出すようにしてやりましょう。

[TestMethod]
publicvoid TestExecuteFolderPathCore()
{
    _form.Dynamic().ExecuteFolderPathCore(@"C:\Program Files");
    //フォームのテキストをチェック
    Assert.IsTrue(string.Compare(@"C:\Program Files", (string)_form.Dynamic().Text, true) == 0);
}

この辺がFriendlyの醍醐味です。インターフェイスコンポーネントテストや単体テストのように自由に選択、変更できるので、効率の悪い箇所を削って費用対効果の高い自動化が実現できるのです。
こちらは、そんな感じの話のスライドです。

次回はWPFの上位ライブラリやります。

Friendly ハンズオン 11 WPF用上位ライブラリ

$
0
0

今回はWPFGUIコントロールの操作です。
これは、めとべやさんと協力して作っています。ソースコードはこちらです。
Roommetro/Friendly.WPFStandardControls · GitHub

現在以下のものに対応しています。
WPF用は近いうちに機能追加していこうと思っています。
追加したらまたブログに書きますねー。

対応表

System.Windows.ControlsFriendly.WPFStandardControls
ButtonBaseWPFButtonBase
ComboBoxWPFComboBox
ListBoxWPFListBox
ListViewWPFListView
MenuBaseWPFMenuBase
MenuItemWPFMenuItem
ProgressBarWPFProgressBar
RichTextBoxWPFRichTextBox
SelectorWPFSelector
SliderWPFSlider
TabControlWPFTabControl
TextBoxWPFTextBox
ToggleButtonWPFToggleButton
TreeViewWPFTreeView
TreeViewItemWPFTreeViewItem
CalendarWPFCalendar
DatePickerWPFDatePicker
DataGridWPFDataGrid

※System.Windows.Controls.Primitivesのクラスも混じっています。

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

こちらからダウンロードお願いします。
Ishikawa-Tatsuya/HandsOn11 · GitHub

ビルドして対象アプリを起動すると、こんな感じです。
OKボタンを押すと、入力内容がメッセージボックスに表示されます。
f:id:ishikawa-tatsuya:20150110104756p:plain

DataGridの中には、このデータがリストで入っています。

publicenum Language
{
    C,
    CPP,
    CS
}

[Serializable]
publicclass Member
{
    publicstring Name { get; set; }
    public Language Language { get; set; }
    publicbool IsProgramer { get; set; }

    publicoverridestring ToString()
    {
        return Name + ", " + Language + ", " + IsProgramer;
    }
}

Testプロジェクトは参照設定も終わっています。
自分のテストプロジェクトで使う場合は、NugetからFriendly.WPFStandardControlsとFriendly.Windows.NativeStandardControlsを取得してください。NativeStandardControlsはメッセージボックス操作に使います。前回も書きましたが、WPFでもメッセージボックスはネイティブのものが使われているのです。

WPFTest.csコードはこれです。これに、書き足していってください。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.NativeStandardControls;
using RM.Friendly.WPFStandardControls;
using System.Diagnostics;
using System.Windows;
using WpfTarget;
using Codeer.Friendly.Windows.Grasp;

namespace Test
{
    [TestClass]
    publicclass WPFTest
    {
        WindowsAppFriend _app;
        dynamic _main;

        [TestInitialize]
        publicvoid TestInitialize()
        {
            _app = new WindowsAppFriend(Process.Start("WpfTarget.exe"));
            _main = _app.Type<Application>().Current.MainWindow;
        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            Process.GetProcessById(_app.ProcessId).CloseMainWindow();
        }
    }
}

ハンズオン開始

TextBox

まずはTextBoxです。使い方はWinFormsのものと同じですね。
TextBoxの特定はx:Nameでやっています。テスタビリティーを上げるためにプロダクトの方で、全てのコントロールには名前を付けているのですね。

[TestMethod]
publicvoid TestTextBox()
{
    var _textBoxCompanyName = new WPFTextBox(_main._textBoxCompanyName);
    _textBoxCompanyName.EmulateChangeText("Codeer.ltd");
    Assert.AreEqual("Codeer.ltd", _textBoxCompanyName.Text);
}

DatePicker

はい。DatePickerも簡単に操作できますね。

[TestMethod]
publicvoid TestDatePicker()
{
    var _dateTimePickerFounding = new WPFDatePicker(_main._dateTimePickerFounding);
    _dateTimePickerFounding.EmulateChangeDate(new DateTime(2011, 3, 14));
    Assert.AreEqual(new DateTime(2011, 3, 14), _dateTimePickerFounding.SelectedDate);
}

DataGrid

DataGridです。編集方法はText、ComoboBox、CheckBoxに対応しています。その他のものが必要であれば、そんなに難しくはないので、各自で対応してみてください。(基本部分押さえていれば出来ると思います。)
データの検証はここでは、対応するデータでやっていますね。セル文字列を取るインターフェイスもあるのですが、こっちの方が手っ取り早いので。
WPFDataGridのthis[index]はdynamicで相手プロセスの参照が返ってきます。それを実データにキャストすることによって、相手プロセスからコピーを転送してきているのですね。

[TestMethod]
publicvoid TestDataGrid()
{
    var _grid = new WPFDataGrid(_main._grid);
    _grid.EmulateChangeCellText(0, 0, "ishikawa");
    _grid.EmulateChangeCellComboSelect(0, 1, 2);
    _grid.EmulateCellCheck(0, 2, true);

    //1つめのデータアイテムのコピーを取得
    Member member = _grid[0];
    Assert.AreEqual("ishikawa", member.Name);
    Assert.AreEqual(Language.CS, member.Language);
    Assert.AreEqual(true, member.IsProgramer);
}

Button

ボタン操作です。OKボタンを押すと入力内容がメッセージボックスに表示されます。値の設定は、ここまでに書いたものを使い回しています。
表示されるメッセージボックスはWPFでもWin32のものですね。NativeMessageBoxを使用して操作します。

[TestMethod]
publicvoid TestButtonBaseAndMessageBox()
{
    //値の設定
    TestTextBox();
    TestDatePicker();
    TestDataGrid();

    //非同期処理でボタンを押す
    var _buttonOK = new WPFButtonBase(_main._buttonOK);
    Async a = new Async();
    _buttonOK.EmulateClick(a);

    //メッセージボックス(ネイティブ)
    var window = new WindowControl(_main);
    var msg = new NativeMessageBox(window.WaitForNextModal());
    Assert.AreEqual("Codeer.ltd\r\n2011/03/14\r\nishikawa, CS, True", msg.Message);
    msg.EmulateButtonClick("OK");

    //非同期処理完了待ち
    a.WaitForCompletion();
}

GUI要素の特定は?

それはもうx:Nameが一番確実ですw
テスタビリティーを向上させることを考えればx:Nameを付けるのも、なんら恥じることはありません!
・・・
でもまあ嫌な人もいますよね。

Bindingから特定してみる。

結局ですね、相手プロセスでできることは全て可能なのです。だから特定方法も好きなように作ってもらえばOKです。で、その一例としてBindingから特定するサンプルを作ってみました。

コードはこちらから
Ishikawa-Tatsuya/Friendly-SearchByBinding · GitHub
これは、もう少し洗練させて、Friendly.WPFStandardControlsに取り込もうと思っています。乞うご期待!

自分で好きに作れます。

繰り返しますが、特定方法はFriendlyの基本部分とWPFを理解していれば好きなように作れます。
何かいい方法思いついた人はプルリクお願いします。
このブログへのコメントでもOKです!
イデア、要望レベルでも構いません。よろしくです。

次回はTestAssistantの話です。

Friendly ハンズオン 12 TestAssistant(キャプチャリプレイもできますw)

$
0
0

今回はTestAssistantというツールを使ってみます。
タイトルは若干釣りです。
キャプチャリプレイ好きな人もいるのでw

TestAssistantとは

Friendlyとその上位ライブラリを使ったテストの作成の効率を上げるためのツールです。

何ができるの?

意外と多機能なのです。

  • 画面の解析
  • タイプの解析
  • メッセージ解析
  • キャプチャリプレイ
  • スクリプトの実行

C#とFriendlyについての理解が必要です。

「そんな便利なものあるなら先に言ってくれればいいのに」って思うかもしれませんね。でもこれも順番があるのですよ。C#はもちろんFrienldyについての理解が無ければ逆に誤解を与えるのです。ていうか、使いこなせませんしね。最初のころは、このツールもまとめて説明していたので、混乱させてましたね。人によってはこのツールのことをFriendlyって思ったようです。
もちろんそうではなくて、このツールはオマケで、メインはFrienldyというライブラリなのです。(それも基本部分と上位ライブラリに分かれるのですけどね Frienldy いまさら概要と学習計画 - ささいなことですが。)

ハンズオン

操作対象

まずは、WinFormsアプリに使ってみましょう。ハンズオン7で操作したアプリを操作してみます。
ダウンロードはこちらから。
Ishikawa-Tatsuya/HandsOn7 · GitHub
こんなアプリですね。
f:id:ishikawa-tatsuya:20141229145723p:plain

TestAssistantダウンロード

こちらからダウンロードしてください。
ダウンロード - 株式会社Codeer (コーディア)
Zip解凍→自己解凍で使えます。
実はフォルダコピーで使えるものです。自己解凍にしているのは、「ブロック解除」を忘れないためだけです。

起動

解凍フォルダはこんな感じです。
Start_AnyCpu.vbsとStart_x86.vbsがありますね。操作対象のアプリにあった方を実行してください。これ実は単なるショートカットです。TestAssistantフォルダ内にexeが複数入っていて選択するのが面倒なので外に作っただけです。
今回はStart_x86.vbsを実行してください。
f:id:ishikawa-tatsuya:20150110225202p:plain
こんな感じの画面になります。
f:id:ishikawa-tatsuya:20150110225229p:plain

アタッチ

では、アタッチしてみます。
まず、左上のボタンを押してください。そうするとテストプロセス選択ダイアログが表示されますので、TargetFormを選択してください。
f:id:ishikawa-tatsuya:20150110230748p:plain

スクリプト実行

C#スクリプトを実行する機能があります。1関数のみです。実行すると、この関数が呼び出されます。引数に接続されているアプリにアタッチしたWindwsAppFriendが渡ってきますので、それを使って操作します。
ここで簡単に試して、実際のテストコードに持って行くのが一般的な使い方です。一回やってみましょう。このコードを張り付けてください。

WindowControl mainForm = WindowControl.FromZTop(app);
FormsComboBox _comboBox = new FormsComboBox(mainForm.Dynamic()._comboBox);
_comboBox.EmulateChangeSelect(3);

で、実行してみましょう。
f:id:ishikawa-tatsuya:20150111121805p:plain
コンボボックスの選択がかわりましたね。

画面解析

ここからは動画で説明していきます。細かいので、可能ならフルスクリーンで見てください。

TestAssistant 画面解析デモ - YouTube

操作キャプチャ


TestAssistant キャプチャリプレイデモ - YouTube
はい。キャプチャリプレイ'的な'機能です。
これは、あくまで参考程度にとどめておいてください。「このコントロールどう操作するのかなー」って時にやってみて、「あーこのラッパークラス使えばいいのね」って感じです。
それから、知っているラッパークラスの操作をキャプチャでします。そのため、「マウスクリック」とかの低レベルなAPIではなく、「選択変更」のようなリッチなインターフェイスが出力されるのですね。
あと、一画面づつしかキャプチャできません。

不具合解析なんかにも使えます。

内部状態を取得できたり、簡単に内部的な操作を呼び出せるので、ちょっと工夫するとデバッグにも非常に便利に使用できます。実際僕もデバッグにこれを使ったりしています。

次回はアプリケーションドライバについてです。

Friendly ハンズオン 13 アプリケーションドライバ -その1-

$
0
0

今回はアプリケーションドライバの話です。
Frienldyは内部APIを使って効率的に自動化を実現するためのライブラリです。でも、内部APIは文字通り内部仕様にあたるので、テストシナリオにそのまま出てきては、テストのメンテ、作成効率が落ちます。外部仕様が変わらなくても内部仕様が変わっただけでテストが壊れてしまうのです。

また、実際これをテストチームで運用するのは難しいと感じた人もいると思います。それはその通りで、内部APIや内部仕様を有効利用する以上は開発チームのサポートが必須となります。でも、テストシナリオはテストチームで書けた方が効率いいですよね。
アプリケーションドライバは、そのような問題を解決する設計パターンなのです。

アプリケーションドライバとは

対象アプリを操作するためのレイヤです。
これ自体はテストを記述するものではなく、テストをする前段階の「操作」というところに特化した実装を記述するものです。
f:id:ishikawa-tatsuya:20150117171026p:plain

ハンズオン

こちらからダウンロードしてビルドしてください。
Ishikawa-Tatsuya/HandsOn13 · GitHub

対象アプリ

今回の対象は、社員を登録して、住所検索するというアプリです。
EmployeeManagementがそれに当たります。

起動画面です。
f:id:ishikawa-tatsuya:20150117171909p:plain

追加画面です。
f:id:ishikawa-tatsuya:20150117171942p:plain

入力に不備があるとエラーメッセージが表示されます。
f:id:ishikawa-tatsuya:20150117172100p:plain

登録が成功すると、追加ダイアログが消えてメインの画面に戻ります。
メインの画面では、「名前(性別) 住所」でリストに表示されます。
f:id:ishikawa-tatsuya:20150117173604p:plain

検索画面です。
テキストボックスに文字列を入力して実行を押すと、部分一致で検索にヒットしたものがリスト表示されます。
f:id:ishikawa-tatsuya:20150117173906p:plain

検索ヒット
f:id:ishikawa-tatsuya:20150117174120p:plain

該当なし
f:id:ishikawa-tatsuya:20150117174136p:plain

アプリケーションドライバとテストシナリオ

これも、プロジェクトの追加と参照設定までやっています。
EmployeeManagementDriverがアプリケーションドライバで、TestScenarioがテストシナリオです。
では、やってみましょう。

ハンズオン開始

まず、アプリケーションドライバを実装します。
このレイヤの目的はテストシナリオが優雅に記述できるようにするということですね。
内部仕様とか技術的に高度な部分はここで隠蔽します。書き方は、ケースによって異なります。
まずは、GUIマップから隠蔽してみましょう。
このアプリは3つ画面があるので3つクラスを実装します。
EmployeeManagementDriverプロジェクトにそれぞれ追加してください。

メイン画面

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    publicclass MainFormDriver
    {
        public WindowControl Window { get; private set; }
        public FormsListBox ListBoxEmployee { get; private set; }
        public FormsButton ButtonAdd { get; private set; }
        public FormsButton ButtonSearch { get; private set; }

        public MainFormDriver(WindowControl window)
        {
            Window = window;
            ListBoxEmployee = new FormsListBox(Window.Dynamic()._listBoxEmployee);
            ButtonAdd = new FormsButton(Window.Dynamic()._buttonAdd);
            ButtonSearch = new FormsButton(Window.Dynamic()._buttonSearch);
        }
    }
}

追加画面

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    publicclass AddFormDriver
    {
        public WindowControl Window { get; set; }
        public FormsButton ButtonEntry { get; private set; }
        public FormsTextBox TextBoxName { get; private set; }
        public FormsTextBox TextBoxAddress { get; private set; }
        public FormsRadioButton RadioButtonWoman { get; private set; }
        public FormsRadioButton RadioButtonMan { get; private set; }

        public AddFormDriver(WindowControl window)
        {
            Window = window;
            ButtonEntry = new FormsButton(Window.Dynamic()._buttonEntry);
            TextBoxName = new FormsTextBox(Window.Dynamic()._textBoxName);
            TextBoxAddress = new FormsTextBox(Window.Dynamic()._textBoxAddress);
            RadioButtonWoman = new FormsRadioButton(Window.Dynamic()._radioButtonWoman);
            RadioButtonMan = new FormsRadioButton(window.Dynamic()._radioButtonMan);
        }
    }
}

検索画面

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    publicclass SearchFormDriver
    {
        public WindowControl Window { get; set; }
        public FormsButton ButtonExecute { get; private set; }
        public FormsTextBox TextBoxSearch { get; private set; }
        public FormsListBox ListBoxEmployee { get; private set; }

        public SearchFormDriver(WindowControl window)
        {
            Window = window;
            ButtonExecute = new FormsButton(Window.Dynamic()._buttonExecute);
            TextBoxSearch = new FormsTextBox(Window.Dynamic()._textBoxSearch);
            ListBoxEmployee = new FormsListBox(Window.Dynamic()._listBoxEmployee);
        }
    }
}

何も難しことはしていませんね。単に画面要素の取得をラップしているだけです。
あと一つ追加します。アプリケーション自体の操作のクラスです。AppDriverという名前にしますか。

アプリケーション自体の操作

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System.Diagnostics;

namespace EmployeeManagementDriver
{
    publicclass AppDriver
    {
        WindowsAppFriend _app;
        public MainFormDriver MainForm { get; private set; }

        public AppDriver()
        {
            var process = Process.Start("EmployeeManagement.exe");
            _app = new WindowsAppFriend(process);
            MainForm = new MainFormDriver(new WindowControl(_app, process.MainWindowHandle));
        }

        publicvoid Release()
        {
            Process.GetProcessById(_app.ProcessId).CloseMainWindow();
        }
    }
}

このままで良いでしょうか?ちょっと使ってみましょう。上記の仕様の操作がテストシナリオから簡単にできればOKです。
調整用のテストコードをTestScenarioにAdjustDriverと言う名前で追加します。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.NativeStandardControls;

namespace TestScenario
{
    [TestClass]
    publicclass AdjustDriver
    {
        AppDriver _app;

        [TestInitialize]
        publicvoid TestInitialize()
        {
            _app = new AppDriver();
        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            _app.Release();
        }
    }
}

では、追加コードを検討してみましょう。
まずは、成功するケース。

[TestMethod]
publicvoid TestAddSuccess()
{
    var async = new Async();
    _app.MainForm.ButtonAdd.EmulateClick(async);

    var addForm = new AddFormDriver(_app.MainForm.Window.WaitForNextModal());

    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    addForm.RadioButtonMan.EmulateCheck();
    addForm.ButtonEntry.EmulateClick();

    async.WaitForCompletion();

    Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee.Dynamic().Items[0].ToString());
}

で、失敗するケース。

[TestMethod]
publicvoid TestAddError()
{
    var asyncAddForm = new Async();
    _app.MainForm.ButtonAdd.EmulateClick(asyncAddForm);

    var addForm = new AddFormDriver(_app.MainForm.Window.WaitForNextModal());

    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");

    var asyncMsg = new Async();
    addForm.ButtonEntry.EmulateClick(asyncMsg);
    var msgBox = new NativeMessageBox(addForm.Window.WaitForNextModal());

    Assert.AreEqual("性別を入力してください。", msgBox.Message);
    msgBox.EmulateButtonClick("OK");
    asyncMsg.WaitForCompletion();

    addForm.Window.Dynamic().Close();

    asyncAddForm.WaitForCompletion();
}

どうでしょう?
一見スッキリしているような気がしますが、問題があります。
①内部API(内部仕様)を使っている
②若干難易度が高い

①に関してはルール違反ですね。テストシナリオではこれは使ってはいけません。

_app.MainForm.ListBoxEmployee.Dynamic().Items[0].ToString()
addForm.Window.Dynamic().Close();

内部APIを使うこと自体はOKです。上位ライブラリでラップ提供されていないものは多く存在するので、その場合は内部APIを使えば良いのです。そしてFriendlyはそれが簡単にできます。
ただ、それはアプリケーションドライバ層でラップしてテストシナリオに提供してください。

②に関しては、微妙なとこです。テストシナリオを書く人のスキルによります。
テスト自動化は最終的にはプログラムになるので、技術的な要素は必ず入ります。でも、テストシナリオではもう少し隠れていた方がいいですよね。と言うのはできれば、テストチームの人でも書けた方が良いのです。テストチームの人はテストのスペシャリストであってプログラムは専門と言うわけではありません。(時々、プログラムスキルも高い人もいますが)その人たちの協力も得れるようにしておきたいのです。

先ほどのテストシナリオのTestAddで、難しいかなーというポイントを挙げてみます。

  • Async
  • new AddFormDriver

Asyncというかモーダルダイアログのコントロールですね。そこを実装したプログラマーからすると当たり前なのですが、ボタンを押した処理が固まっているというのは理解しづらいもののようです。(WaitForCompletionでの待ち合わせも)
後は、new AddFormDriverですね。これは何かというとAddFormDriverと言う名前を見つけるのが面倒なのです。他の処理を見てみると全てインテリセンスで解決できています。まあ、全てのnewをなくすことは実際難しいかもしれませんが、インテリセンスで解決できると難易度は格段に下がります。これはポイントですね。

今回はここまで

長くなりましたね。今回はここまでにします。
次回はこれを調整していきます。

Friendly ハンズオン 13 アプリケーションドライバ -その2-

$
0
0

前回の続きです。読んでない方は、そちらを先に読んでください。
前回作ったアプリケーションドライバでは「内部仕様」と「操作技術」の隠蔽度が不十分ってことでしたね。
では改善してみます。

内部仕様の隠蔽

_app.MainForm.ListBoxEmployee.Dynamic().Items[0].ToString()
addForm.Window.Dynamic().Close();

Dynamic()は隠蔽しましょう。
単にラッパー関数を作ればよいだけです。関数名は外部仕様からわかる名前が良いですね。僕はやったことありませんが、日本語の関数名でもいいかもしれません。目的は開発チーム以外でも理解できるインターフェイスを提供することなのです。
経験的にコントロールの型から始めると、使う人は探しやすいようです。(少人数からのアンケート結果です)命名規則はプロジェクトごとに最適なものにしていただければ。

[MainFormDriver.cs]

publicstring ListBoxEmployee_GetItemText(int index)
{
    return ListBoxEmployee.Dynamic().Items[index].ToString();
}

こちらはAddFormを閉じるということなのでプレフィックス的なのはいらないですね。

[AddFormDriver.cs]

publicvoid Close()
{
    Window.Dynamic().Close();
}

技術的難易度の高い部分の隠蔽

テストシナリオは簡単に書きたいのです。難易度の高い文法も隠蔽しましょう。
ここでは、モーダルダイアログ対応ですね。
AddFormはモーダルダイアログとして表示されると仕様で決まっているので、ここに表示されるトリガとなった処理のAsyncを持たせます。
まあ、コードを見ると早いですね。

//関連するとこだけ、書いています。publicclass AddFormDriver
{
    Async Async { get; set; }

    //...その他の定義public AddFormDriver(WindowControl window, Async async)
    {
        Async = async;
        //...その他の定義
   }

    //...その他の定義publicvoid ButtonEntry_EmulateClickAndClose()
    {
        ButtonEntry.EmulateClick();
        Async.WaitForCompletion();
    }

    publicvoid Close()
    {
        Window.Dynamic().Close();
        Async.WaitForCompletion();
    }
}

[MainFormDriver]

public AddFormDriver ButtonAdd_EmulateClick()
{
    var async = new Async();
    ButtonAdd.EmulateClick(async);
    returnnew AddFormDriver(Window.WaitForNextModal(), async);
}

これによって、追加のテストシナリオはこんな感じで書けるようになりました!

[TestMethod]
publicvoid TestAdd()
{
    var addForm = _app.MainForm.ButtonAdd_EmulateClick();
    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    addForm.RadioButtonMan.EmulateCheck();
    addForm.ButtonEntry_EmulateClickAndClose();
    Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee_GetItemText(0));
}

これなら、全てインテリセンスが効くので専門職のプログラマでなくても書けるのではないでしょうか?

もう一つ、追加でエラーメッセージが表示されるパスがありますね。そちらもメッセージボックスがモーダルで出る仕様なので、隠蔽します。メッセージボックス自体を隠蔽しても良いと思います。と言うのは今回の仕様だとメッセージボックスの操作はOKボタンだけなので、シナリオで操作する必要はないのです。

[AddFormDriver.cs]

publicstring ButtonEntry_EmulateClickAndGetMessage()
{
    Async async = new Async();
    ButtonEntry.EmulateClick(async);
    var msgBox = new NativeMessageBox(Window.WaitForNextModal());
    var msg = msgBox.Message;
    msgBox.EmulateButtonClick("OK");
    async.WaitForCompletion();
    return msg;
}

テストシナリオはこう書けます。

[TestMethod]
publicvoid TestError()
{
    var addForm = _app.MainForm.ButtonAdd_EmulateClick();
    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    Assert.AreEqual("性別を入力してください。", addForm.ButtonEntry_EmulateClickAndGetMessage());
    addForm.Close();
}

これも、随分と簡単になりましたね。

ショートカット

次は、検索のテストのためのインターフェイスも考えてみましょう。追加画面とほぼ同じやり方で作れます。
でも、検索にはセットアップとしてデータ追加が必要になってきますよね。TestAddで使ったインターフェイスを利用して追加してもいいのですが、ちょっと書き方が煩雑です。この場合、僕ならMainFormDriverに、こんなインターフェイスを提供します。

publicvoid AddEmployeeData(params EmployeeData[] datas)
{
    ListBoxEmployee.Dynamic().Items.AddRange(datas);
}

検索のテストの場合、テストしたいのは検索画面からです。テストデータのセットアップはテスト対象外なのです。画面を操作して、100件追加するだけで200秒程度はかかると思います。1万件とかならもう非現実的な時間になります。でも、内部仕様を知っていて、データの型と格納するバッファがわかっていれば、そこにデータを詰めれば良いのです。これなら一瞬ですね。
テスト対象外の部分ならこんな手法でOKです。
ちょっと、実験してみましょう。PCスペックにもよりますが、1万件でも1秒くらいですね。

[TestMethod]
publicvoid TestAddShortcut()
{
    List<EmployeeData> data = new List<EmployeeData>();
    for (int i = 0; i < 10000; i++)
    {
        data.Add(new EmployeeData()
            {
                Name = "Name" + i.ToString(),
                Address = "Osaka-" + i.ToString(),
                IsMan = i % 2 == 0
            });
    }
    _app.MainForm.AddEmployeeData(data.ToArray());
}

今回は書きませんが、テスト対象の場合でも、時間的な都合や設計、本当にテストしたい部分を考慮すると、GUI層のを省いてもいい時ってあります。こちらで発表しましたのでよろしければ見てください。

次回はエラー時の対応です。

ここまででアプリケーションドライバの画面への対応部分は実装できました。
これで正常系はテストできます。
でも、実は問題があるのです。実際にデグレが発生すると、予想外のところでモーダルダイアログが表示されて、テストが固まるのです。次の朝来たら、「一つ目のテストで固まってて状況がわからん。」なんてことがあります。次回はその対応をやります。

ここまでの全文を貼っておきます。

参考までに、ここまでの実装を貼っておきます。
慣れると、これくらいは簡単に書けるようになります。何か画面を実装したときには一緒にアプリケーションドライバにも対応する操作クラスを増やすようにすると良いと思います。

アプリケーション操作クラス

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System.Diagnostics;

namespace EmployeeManagementDriver
{
    publicclass AppDriver
    {
        WindowsAppFriend _app;
        public MainFormDriver MainForm { get; private set; }

        public AppDriver()
        {
            var process = Process.Start("EmployeeManagement.exe");
            _app = new WindowsAppFriend(process);
            MainForm = new MainFormDriver(new WindowControl(_app, process.MainWindowHandle));
        }

        publicvoid Release()
        {
            Process.GetProcessById(_app.ProcessId).CloseMainWindow();
        }
    }
}

メイン画面操作クラス

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using EmployeeManagement;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    publicclass MainFormDriver
    {
        WindowControl Window { get; set; }
        public FormsListBox ListBoxEmployee { get; set; }
        public FormsButton ButtonAdd { get; set; }
        public FormsButton ButtonSearch { get; set; }

        public MainFormDriver(WindowControl window)
        {
            Window = window;
            ListBoxEmployee = new FormsListBox(window.Dynamic()._listBoxEmployee);
            ButtonAdd = new FormsButton(window.Dynamic()._buttonAdd);
            ButtonSearch = new FormsButton(window.Dynamic()._buttonSearch);
        }

        public AddFormDriver ButtonAdd_EmulateClick()
        {
            var async = new Async();
            ButtonAdd.EmulateClick(async);
            returnnew AddFormDriver(Window.WaitForNextModal(), async);
        }

        public SearchFormDriver ButtonSearch_EmulateClick()
        {
            var async = new Async();
            ButtonSearch.EmulateClick(async);
            returnnew SearchFormDriver(Window.WaitForNextModal(), async);
        }

        publicstring ListBoxEmployee_GetItemText(int index)
        {
            return ListBoxEmployee.Dynamic().Items[index].ToString();
        }

        publicvoid AddEmployeeData(params EmployeeData[] datas)
        {
            ListBoxEmployee.Dynamic().Items.AddRange(datas);
        }
    }
}

追加画面操作クラス

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Codeer.Friendly.Windows.NativeStandardControls;
using Ong.Friendly.FormsStandardControls;
using System;

namespace EmployeeManagementDriver
{
    publicclass AddFormDriver
    {
        Async Async { get; set; }
        public WindowControl Window { get; set; }
        public FormsButton ButtonEntry { get; set; }
        public FormsTextBox TextBoxName { get; private set; }
        public FormsTextBox TextBoxAddress { get; private set; }
        public FormsRadioButton RadioButtonWoman { get; private set; }
        public FormsRadioButton RadioButtonMan { get; private set; }

        public AddFormDriver(WindowControl window, Async async)
        {
            Window = window;
            Async = async;
            ButtonEntry = new FormsButton(Window.Dynamic()._buttonEntry);
            TextBoxName = new FormsTextBox(Window.Dynamic()._textBoxName);
            TextBoxAddress = new FormsTextBox(Window.Dynamic()._textBoxAddress);
            RadioButtonWoman = new FormsRadioButton(Window.Dynamic()._radioButtonWoman);
            RadioButtonMan = new FormsRadioButton(Window.Dynamic()._radioButtonMan);
        }

        publicstring ButtonEntry_EmulateClickAndGetMessage()
        {
            Async async = new Async();
            ButtonEntry.EmulateClick(async);
            var msgBox = new NativeMessageBox(Window.WaitForNextModal());
            var msg = msgBox.Message;
            msgBox.EmulateButtonClick("OK");
            async.WaitForCompletion();
            return msg;
        }

        publicvoid ButtonEntry_EmulateClickAndClose()
        {
            ButtonEntry.EmulateClick();
            Async.WaitForCompletion();
        }

        publicvoid Close()
        {
            Window.Dynamic().Close();
            Async.WaitForCompletion();
        }
    }
}

検索画面操作クラス

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System;
using System.Linq;
using System.Windows.Forms;

namespace EmployeeManagementDriver
{
    publicclass SearchFormDriver
    {
        Async Async { get; set; }
        public WindowControl Window { get; set; }
        public FormsButton ButtonExecute { get; private set; }
        public FormsTextBox TextBoxSearch { get; private set; }
        public FormsListBox ListBoxEmployee { get; private set; }

        public SearchFormDriver(WindowControl window, Async async)
        {
            Async = async;
            Window = window;
            ButtonExecute = new FormsButton(Window.Dynamic()._buttonExecute);
            TextBoxSearch = new FormsTextBox(Window.Dynamic()._textBoxSearch);
            ListBoxEmployee = new FormsListBox(Window.Dynamic()._listBoxEmployee);
        }

        publicvoid Close()
        {
            Window.Dynamic().Close();
            Async.WaitForCompletion();
        }

        publicstring[] ListBoxEmployee_GetSearchResult()
        {
            WindowsAppExpander.LoadAssembly(Window.App, GetType().Assembly);
            return Window.App.Type(GetType()).GetListItems(ListBoxEmployee);
        }

        staticstring[] GetListItems(ListBox listBox)
        {
            return listBox.Items.Cast<object>().Select(e => e.ToString()).ToArray();
        }
    }
}

アプリケーションドライバ調整用テストコード

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.NativeStandardControls;
using System.Diagnostics;
using System.Collections.Generic;
using EmployeeManagement;

namespace TestScenario
{
    [TestClass]
    publicclass AdjustDriver
    {
        AppDriver _app;

        [TestInitialize]
        publicvoid TestInitialize()
        {
            _app = new AppDriver();

        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            _app.Release();
        }

        [TestMethod]
        publicvoid TestAdd()
        {
            var addForm = _app.MainForm.ButtonAdd_EmulateClick();
            addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
            addForm.TextBoxAddress.EmulateChangeText("Japan");
            addForm.RadioButtonMan.EmulateCheck();
            addForm.ButtonEntry.EmulateClick();
            Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee_GetItemText(0));
        }

        [TestMethod]
        publicvoid TestError()
        {
            var addForm = _app.MainForm.ButtonAdd_EmulateClick();
            addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
            addForm.TextBoxAddress.EmulateChangeText("Japan");
            Assert.AreEqual("性別を入力してください。", addForm.ButtonEntry_EmulateClickAndGetMessage());
            addForm.Close();
        }

        [TestMethod]
        publicvoid TestAddShortcut()
        {
            List<EmployeeData> data = new List<EmployeeData>();
            for (int i = 0; i < 10000; i++)
            {
                data.Add(new EmployeeData()
                    {
                        Name = "Name" + i.ToString(),
                        Address = "Osaka-" + i.ToString(),
                        IsMan = i % 2 == 0
                    });
            }
            _app.MainForm.AddEmployeeData(data.ToArray());
        }
    }
}

サンプルコード修正

2015/1/30
モーダルダイアログの書き方が適切でなかったので修正しました。
詳細は後日にダイアログ表示のパターン編とかやって解説します。

Friendly ハンズオン 13 アプリケーションドライバ -その3-

$
0
0

前回までで、通常操作に関しての実装は完了して、思い通りにアプリを操作できるところまで行きました。
で、今回はテスト自動化で困りがちな点の対応を考えていきます。これもアプリケーションドライバ層で吸収すれば良いと思います。

ここから先はAppDriverクラスを拡張していきます。
そんなに難しいことではなくチップス的なテクニックです。

モーダルで固まった場合の対応

タイムアウトさせるしかないですね。
時間が来たら対象を強制終了します。
こんな感じです。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EmployeeManagementDriver
{
    publicclass Killer
    {
        Task _killer;
        bool _executing = true;
        bool _kill;
        publicint Timeup { get; set; }

        public Killer(int timeup, int processId)
        {
            Timeup = timeup;
            _killer = Task.Factory.StartNew(() =>
            {
                Stopwatch watch = new Stopwatch();
                watch.Start();
                while (_executing)
                {
                    if (Timeup < watch.ElapsedMilliseconds)
                    {
                        Process.GetProcessById(processId).Kill();
                        _kill = true;
                        break;
                    }
                    Thread.Sleep(10);
                }
            });
        }

        publicbool Finish()
        {
            _executing = false;
            _killer.Wait();
            return !_kill;
        }
    }
}

AppDriverで使ってみましょう。

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System.Diagnostics;

namespace EmployeeManagementDriver
{
    publicclass AppDriver
    {
        WindowsAppFriend _app;
        public MainFormDriver MainForm { get; private set; }
        public Killer Killer{ get; private set; }

        public AppDriver()
        {
            var process = Process.Start("EmployeeManagement.exe");
            _app = new WindowsAppFriend(process);
            MainForm = new MainFormDriver(new WindowControl(_app, process.MainWindowHandle));

            //5分でタイムアウトにする
            Killer = new Killer(1000 * 60 * 5, process.Id);
        }

        publicvoid Release()
        {
            if (Killer.Finish())
            {
                //タイムアップ終了していない場合は自ら終了させる
                Process.GetProcessById(_app.ProcessId).CloseMainWindow();
            }
        }
    }
}

時間は共通で定義しておいて、特別なテストだけ延長するとかにすると良いと思います。
あんまり、細かく設定するのは面倒ですので、テストが複雑化してしまいます。
正常パスでは発生しないケースなので。
ちょっとテストしておきましょう。

[TestMethod]
publicvoid TestTimeup()
{
    _app.Killer.Timeup = 1000;
    try
    {
        var addForm = _app.MainForm.ButtonAdd_EmulateClick();
        addForm.ButtonEntry_EmulateClickAndClose();
        Assert.Fail();
    }
    catch (FriendlyOperationException) { }
}

これで、モーダルダイアログで止まっても、5分後には次のテストに移れます。

実はもう一つやることがあります。

それは、対象アプリのライフサイクルの管理です。
次回はそれをやります。

サンプルコード修正

2015/1/30
モーダルダイアログの書き方が適切でなかったので修正しました。それにより、一部ここに書くのは不適当な記事を削除しました。また別の機会に解説します。


Friendly ハンズオン 13 アプリケーションドライバ -その4-

$
0
0

前回の続きです。

今回は対象アプリのライフサイクルの管理に関してやります。

あれ?でも今までも起動と終了を管理してましたよね。
それだけではダメなんでしょうか?

もちろん、今までのでもOKです。
一つのテストごとにexeを起動しなおすと、前回のテストに影響されずにテストができます。バッチリです。
でも・・・

大きなアプリだと起動と終了は遅い

だいたい、システムテストまで自動化したいと考えるのは大きなアプリであることが多いですね。
そして、そのようなアプリは起動も終了も大抵遅いのです。
平均で5秒と考えても1000ケースやたら1時間の無駄ですね。
なんとかならないでしょうか?

アプリの終了のタイミング

基本は、一つのアプリでテストを実行させ続けるようにして、以下のタイミングのどちらかで再起動します。
①テストが失敗した場合
②クラスに定義されたテストが完了したとき

①に関しては、テストが失敗した場合は内部状態が不正になっている可能性が高いからです。前回のテストの影響をうけます。これはあまりよろしくないですね。だから一度終了させます。もちろん成功している場合だって前のテストの影響を受けていることもあります。これはトレードオフなのです。

②に関してはまさにそれで、成功しているとは言え、長時間実行させ続けると、何か変な状態になる可能性があるので、クラス単位では終了させると。
「え、それって見つけたいんじゃないの?」って思うかもしれませんね。でも、そういうので不具合を見つけたとしても原因の解明が難しいのです。で、テストが不安定になり、本来そのテストで確認したいこと以外に大量に工数がとられて費用対効果が悪くなってしまうのです。

もちろんそういうのを見つけるために組んだ自動テストなら問題ないし、そのようなランニングテストもやっておくべきですが、それを通常のテストケースで一緒にやるのは避けた方が良いでしょう。テストにはそれぞれ目的があるのです。

これは一つの案です。

必ずこうしてくださいってわけではありません。時間が許すなら、毎回再起動したほうがスッキリしますよね。でも、大量にテストを実施する場合は、こんな方法もあるんだよなーって、候補の一つとして検討してみてください。

実装方法

これを実現するために必要なことは次のものです。

  • テストの成否をコード中で知る
  • クラスの初期化と終了のタイミング

VSTestでは、こんなコードで知ることができます。
まあ、「なんでstaticやねん!」って思うかもしれませんが、こういうものなので・・・。

[ClassInitialize]
publicstaticvoid ClassInitialize(TestContext c)
{
}

[ClassCleanup]
publicstaticvoid ClassCleanup()
{
}

AppDriverもこのライフサイクル管理の仕様に合わせて書き換えてみましょう。
アタッチしたときに何らかの方法で初期状態にする必要がありますね。
もしくは、各テスト終了時は初期状態に戻すようにするか。
この辺は各アプリごとに初期化、終了方法を考えてみてください。

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System.Diagnostics;

namespace EmployeeManagementDriver
{
    publicclass AppDriver
    {
        Process _process;
        WindowsAppFriend _app;
        public MainFormDriver MainForm { get; private set; }
        public Killer Killer;

        publicvoid Attach()
        {
            if (_process == null)
            {
                _process = Process.Start("EmployeeManagement.exe");
            }
            _app = new WindowsAppFriend(_process);
            MainForm = new MainFormDriver(new WindowControl(_app, _process.MainWindowHandle));

            //アプリを初期状態に戻す
            InitApp();

            Killer = new Killer(1000 * 60 * 5, _process.Id);
        }

        publicvoid Release(bool isContinue)
        {
            if (isContinue)
            {
                Killer.Finish();
                _app.Dispose();
            }
            else
            {
                EndProcess();
            }
        }

        privatevoid InitApp()
        {
            MainForm.ListBoxEmployee.Dynamic().Items.Clear();
        }

        publicvoid EndProcess()
        {
            try
            {
                _killer.Finish();
            }
            catch { }
            try
            {
                _process.Kill();
            }
            catch { }
            _process = null;
        }
    }
}

で、テストシナリオもこれに合わせて書き換えてみます。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.NativeStandardControls;
using System.Diagnostics;
using System.Collections.Generic;
using EmployeeManagement;
using System.Linq;

namespace TestScenario
{
    [TestClass]
    publicclass AdjustDriver
    {
        static AppDriver _app;
        static Dictionary<string, bool> _tests;
        public TestContext TestContext { get; set; }

        [ClassInitialize]
        publicstaticvoid ClassInitialize(TestContext c)
        {
            _app = new AppDriver();
            _tests = typeof(AdjustDriver).GetMethods().Where(e => 0< e.GetCustomAttributes(typeof(TestMethodAttribute), true).Length).ToDictionary(e => e.Name, e => true);
        }

        [ClassCleanup]
        publicstaticvoid ClassCleanup()
        {
            _app.EndProcess();
        }

        [TestInitialize]
        publicvoid TestInitialize()
        {
            _app.Attach();
        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            if (TestContext.DataRow == null ||
                ReferenceEquals(TestContext.DataRow, TestContext.DataRow.Table.Rows[TestContext.DataRow.Table.Rows.Count - 1]))
            {
                _tests.Remove(TestContext.TestName);
            }
            _app.Release(TestContext.CurrentTestOutcome == UnitTestOutcome.Passed && 0< _tests.Count);
        }

        [TestMethod]
        publicvoid TestAdd()
        {
            var addForm = _app.MainForm.ButtonAdd_EmulateClick();
            addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
            addForm.TextBoxAddress.EmulateChangeText("Japan");
            addForm.RadioButtonMan.EmulateCheck();
            addForm.ButtonEntry.EmulateClick();
            Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee_GetItemText(0));
        }

        [TestMethod]
        publicvoid TestError()
        {
            var addForm = _app.MainForm.ButtonAdd_EmulateClick();
            addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
            addForm.TextBoxAddress.EmulateChangeText("Japan");
            Assert.AreEqual("性別を入力してください。", addForm.ButtonEntry_EmulateClickAndGetMessage());
            addForm.Close();
        }

        [TestMethod]
        publicvoid TestAddShortcut()
        {
            List<EmployeeData> data = new List<EmployeeData>();
            for (int i = 0; i < 10000; i++)
            {
                data.Add(new EmployeeData()
                {
                    Name = "Name" + i.ToString(),
                    Address = "Osaka-" + i.ToString(),
                    IsMan = i % 2 == 0
                });
            }
            _app.MainForm.AddEmployeeData(data.ToArray());
        }

        [TestMethod]
        publicvoid TestTimeup()
        {
            if (_app.IsDebug)
            {
                return;
            }

            _app.SetTimeup(1000);
            try
            {
                var addForm = _app.MainForm.ButtonAdd_EmulateClick();
                addForm.ButtonEntry_EmulateClickAndClose();
                Assert.Fail();
            }
            catch (FriendlyOperationException)
            {
                _app.EndProcess();
            }
        }
    }
}

プロセス管理に関してもう一つ工夫があります。

それは、対象プロセスのデバッグです。
でも、長くなったんで次回に続く・・・

サンプルコード修正

4/29 TestCleanupの処理を修正しました。

Friendly ハンズオン 13 アプリケーションドライバ -その5-

$
0
0

前回の続きです。
あと、ひと手間かけてやりましょう。それは対象プロセスのデバッグです。

デグレ検出→調査したい

自動検査でデグレを検出しました!作った甲斐がありましたね!
では調査しましょう。もちろん対象プロセスはデバッガから起動したもので調査したいですね。

あれ?EXE起動から始まってる・・・

あー、今の方式だと、EXEを起動させてテスト実行開始してますね。まあ、アタッチすればよいのですが面倒です。それに、ゆっくりデバッグしてたらタイムアップで終了させられてしまいますよねw。
こんな仕様でどうでしょうか?

  • デバッグ起動したプロセスがあればそれにアタッチ
  • テスト実行後もプロセスを終了させない
  • タイムアウト終了もしない

調査中は勝手にプロセス終了されたくないですよね。情報が減るので。ではこれで実装してみましょう。AppDriverのプロセス管理の部分を通常用とデバッグ用に分けたらいいですね。こんな時はストラテジーパターン的なアプローチがいいですね。

通常用とデバッグ用で異なる部分を抽出し、インターフェイス化して切り分けるようにします。差分が出るのは、プロセスのライフサイクル管理の部分ですね。

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;

namespace EmployeeManagementDriver
{
    publicclass AppDriver
    {
        IAppDriverCore _core;
        WindowsAppFriend _app;

        public MainFormDriver MainForm { get; private set; }
        publicbool IsDebug { get { return _core is AppDriverDebug; } }

        public AppDriver()
        {
            _core = AppDriverDebug.Exists ? (IAppDriverCore)new AppDriverDebug() : new AppDriverNormal();
        }

        publicvoid Attach()
        {
            _core.Attach();
            _app = new WindowsAppFriend(_core.Process);
            MainForm = new MainFormDriver(new WindowControl(_app, _core.Process.MainWindowHandle));
            InitApp();
        }

        publicvoid SetTimeup(int time)
        {
            _core.SetTimeup(time);
        }

        publicvoid Release(bool isContinue)
        {
            _app.Dispose();
            _core.Release(isContinue);
        }

        privatevoid InitApp()
        {
            MainForm.ListBoxEmployee.Dynamic().Items.Clear();
        }

        publicvoid EndProcess()
        {
            _core.EndProcess();
        }
    }
}

差分はこんな感じでインターフェイス化されます。

using System.Diagnostics;

namespace EmployeeManagementDriver
{
    interface IAppDriverCore
    {
        Process Process { get; }
        void SetTimeup(int time);
        void Attach();
        void Release(bool isSuccess);
        void EndProcess();
    }
}

通常時用ですね。
前回のコードから抽出しただけです。

using System.Diagnostics;

namespace EmployeeManagementDriver
{
    class AppDriverNormal : IAppDriverCore
    {
        public Process Process { get; private set; }
        Killer _killer;

        publicvoid Attach()
        {
            if (Process == null)
            {
                Process = Process.Start("EmployeeManagement.exe");
            }
            _killer = new Killer(1000 * 60 * 5, Process.Id);
        }

        publicvoid Release(bool isContinue)
        {
            if (isContinue)
            {
                _killer.Finish();
            }
            else
            {
                EndProcess();
            }
        }

        publicvoid SetTimeup(int time)
        {
            _killer.Timeup = time;
        }

        publicvoid EndProcess()
        {
            try
            {
                _killer.Finish();
            }
            catch { }
            try
            {
                Process.Kill();
            }
            catch { }
            Process = null;
        }
    }
}

デバッグ用です。.Netアプリの場合は[vshost]ってのが名前につくのでそれで区別しましょう。ネイティブアプリの場合はまた別の方法でやってみてください。まあこれはプロジェクトに応じた方法で。

これは終了処理系は何にもやらないですね。
デバッガで起動しているものを勝手に終了させる必要はないのです。

using System;
using System.Diagnostics;
using System.Linq;

namespace EmployeeManagementDriver
{
    class AppDriverDebug : IAppDriverCore
    {
        public Process Process { get; private set; }

        publicstaticbool Exists { get { return GetDebugProcess() != null; } }

        publicvoid Attach()
        {
            Process = GetDebugProcess();
        }
        publicvoid Release(bool isContinue) { }
        publicvoid SetTimeup(int time) { }
        publicvoid EndProcess() { }

        static Process GetDebugProcess()
        {
            return Process.GetProcessesByName("EmployeeManagement.vshost").Where(e => e.MainWindowHandle != IntPtr.Zero).FirstOrDefault();
        }
    }
}

EmployeeManagementのslnファイルを作る

面倒だったんで一つのソリューションでやりましたが、デバッグを考えるとプロダクトだけのソリューションファイルがあった方がいいですね。ソリューションファイルを二つメンテするのはちょっと手間ですが、プロジェクトファイルを追加するときに両方に追加するだけなので、まあアリかなと。
嫌な場合は、テストコードと共有する型を含んだdllだけ参照するとかもアリです。実際そうしているお客さんもいます。

まあ、今回はslnファイルをもう一つ作りましょう。次の手順で作れます。
①一旦VisualStudioを閉じる
②HandsOn13.slnの拡張子を一回消す
③EmployeeManagement.csprojをダブルクリック
④全保存ボタンを押す→slnファイルを任意の名前で保存する
⑤HandsOn13の拡張子を戻す

②の手順がいるのは、このファイルあったらEmployeeManagement.csprojから起動しても、そのslnファイルを使っちゃうんですよねー。

では、EmployeeManagement.exeをデバッグ実行してテストを実行してみてください。
ちゃんとそのEXEにアッタッチされましたよね?

備考

それから、もう一つ、このサンプルでの手抜きを言っておくと、プロセス起動でEXEを直接参照しているので、同一フォルダにEXEがあって、それを起動していますが、実際はこんなことしないですよ。どこか別のフォルダ置かれたファイルを起動させます。パスは相対パスにして、そのルールはプロジェクトごとに決めてください。

これで、一旦アプリケーションドライバの話は終わりです。

どうだったでしょうか?
気が付いたかもしれませんが、Friendly関係ない部分も多かったですね。
アプリケーションドライバ作成時はこの辺の.Netの知識も必要になってきます。
でも、それは開発者ならおそらくは知っているはずのことです。
(だって、対象アプリ作りましたよね?)
だから、その人たちに作ってもらいましょう。

長く説明しましたが、ボリューム自体はそんなに大きくないですね。
最終形はこちらからダウンロードできます。

次回からはテストシナリオを書いてみます。

はい。ここまでで作ったアプリケーションドライバを使ってテストシナリオを書いてみます。
アプリケーションドライバの実装を頑張ったので、そちらは簡単に作れますよー。

Friendly2.5.0とFriendly.Windows2.5.1をリリースしました。

$
0
0

久しぶりのリリースです。
NuGet Gallery | Packages matching codeer

まあ、表題のもの以外も参照バージョンが変わるので全部更新しているのですがw
ちょっと解説します。
解説用のコードはこちらからダウンロードできます

何ができるようになったかと言うと・・・

  • dynamicを引数にとった時の戻り値対応
  • デフォルト以外のドメインへのアタッチ

dynamicを引数にとった時の戻り値対応

ExplicitAppVarと言うクラスを追加しました。
えーとショボイです。単にAppVarをラップしただけです。
なにかというと、dynamicなんですよね。
dynamicの変数を引数に渡すと、戻り値もdynamicになるんですよ。
例えばこんな感じです。

IEnumerable<int> Func(string s)
{
    returnnewint[0];
}
void Test()
{
    dynamic s = "abc";
    Func(s).//えー、インテリセンスが効かない
}

f:id:ishikawa-tatsuya:20150211014431p:plain
なんでこんなことになるかというと、dynamicを使うとオーバーロードの解決ができないからです。

//dynamicだったら、どっちかわかんないよね。
IEnumerable<int> Func(string s)
{
    returnnewint[0];
}
int Func(int i)
{
    return i;
}

で、Friendlyはdynamicを多用します。そしてFriendlyのコンテキストではdynamicは大抵は対象プロセス内のオブジェクトを指すことが多いです。だからそうでないのがdynamicになると非常に紛らわしいんですよね。
それで、今はないのですが、近いうちにライブラリとしても引数に対象プロセス内のオブジェクトを渡して、戻り値に通常の型を戻すメソッドを追加する予定です。
詳細は確定していませんが、WPFStandardControlsに次のような関数が追加されます。

//イメージstatic WPFDependencyObjectCollection ByBinding(this WPFDependencyObjectCollection collection, string path, ExplicitVar dataItem);

//こんな感じで使う予定//ByBindingの戻り値が勝手にdynamicになったら紛らわしいし、インテリセンス効かなくなる
_window.VisualTree().ByBinding("Hoge", new ExplicitVar(_window.Dynamic().DataContext)).Identify();

まあ、これ(WPFStandardControls)に機能追加したときにもう少し解説します。

デフォルト以外のドメインへのアタッチ

これは凄いですよ!
デフォルト以外のAppDomainにアタッチできるようになりましたw
>誰得?

Friendlyは通常はデフォルトのAppDomainにアタッチします。
だから、複数ドメイン使っているプロジェクトでは操作できない部分があったんですね。
例えばこんなの。

using System;
using System.Windows.Forms;

namespace Target
{
    publicpartialclass MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        //新しいドメインでフォーム起動staticint i = 0;
        publicvoid StartNewDomain()
        {
            AppDomain.CreateDomain("new domain" + i++).DoCallBack(()=>new Form().Show());
        }
    }
}

この新しいフォームに対しては内部APIを今まで呼べませんでした。
だってAppDomainが違うんだもの・・・
しかし!
なんとこれも操作できるようにしたのです。
こんな感じ。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using System.Diagnostics;
using System.Windows.Forms;

namespace Test
{
    [TestClass]
    publicclass AppDomainTest
    {
        WindowsAppFriend _app;

        [TestInitialize]
        publicvoid TestInitialize()
        {
            _app = new WindowsAppFriend(Process.Start("Target.exe"));
        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            Process.GetProcessById(_app.ProcessId).Kill();
        }

        [TestMethod]
        publicvoid Test()
        {
            dynamic form = _app.Type<Application>().OpenForms[0];

            //新しいドメイン開始
            form.StartNewDomain();

            //別のドメインを取得
            WindowsAppFriend[] apps = _app.AttachOtherDomains();

            Assert.AreEqual(1, apps.Length);
            Assert.AreEqual("new domain 0", (string)apps[0].Type<AppDomain>().CurrentDomain.FriendlyName);
        }
    }
}

・・・まあ、複数ドメイン使うGUIアプリなんて、ほとんどないですねw
まあ、もしそういうの作ったら、使ってください。

Friendly ハンズオン 14 -準備- テストシナリオ共通部分抽出

$
0
0

お久しぶりです・・・
だいぶ空いてしまいました。
ブログって一回書かなくなるとダメですね。
気を取り直してテストシナリオの共通部分の抽出行きましょう。

あれ?シナリオ書くって言ってなかった?

はい。
Friendly ハンズオン 13 アプリケーションドライバ -その5- - ささいなことですが。に最後に書いた時はそのつもりだったんですけど、久しぶりに見るとやっぱり共通化しといた方がいいかなーと。あと、見直すとサンプルコードにバグがあったんで修正しときました。(すみません。不具合は見つかったら直しますので、見るたびちょっと変わってるかもです・・・)
Ishikawa-Tatsuya/HandsOn14 · GitHubからダウンロードしてください。このコードを変更していきます。

アプリのライフサイクル管理コード

AdjustDriverのこの部分です。テストシナリオは大体これを書きます。(ライフサイクル管理が異なれば、内容は変わってきます)ちょっと面倒なコードなので共通化しておきます。

static AppDriver _app;
static Dictionary<string, bool> _tests;
public TestContext TestContext { get; set; }

[ClassInitialize]
publicstaticvoid ClassInitialize(TestContext c)
{
    _app = new AppDriver();
    _tests = typeof(AdjustDriver).GetMethods().Where(e => 0< e.GetCustomAttributes(typeof(TestMethodAttribute), true).Length).ToDictionary(e => e.Name, e => true);
}

[ClassCleanup]
publicstaticvoid ClassCleanup()
{
    _app.EndProcess();
}

[TestInitialize]
publicvoid TestInitialize()
{
    _app.Attach();
}

[TestCleanup]
publicvoid TestCleanup()
{
    if (TestContext.DataRow == null ||
        ReferenceEquals(TestContext.DataRow, TestContext.DataRow.Table.Rows[TestContext.DataRow.Table.Rows.Count - 1]))
    {
        _tests.Remove(TestContext.TestName);
    }
    _app.Release(TestContext.CurrentTestOutcome == UnitTestOutcome.Passed && 0< _tests.Count);
}

そもそも何をやっているか?

ポイントは_app.Release(bool isContinue)呼び出しの引数です。テストが終了したときに対象アプリを生き残すか否かです。以下の条件で終了させます。

  1. テスト失敗
  2. クラスに定義したテストメソッドを全て実行した

2.の方を実現するためにClassInitializeに面倒なコード入れてます。(実はこれでも完全ではなく、違うクラスの複数メソッドを選択されて実行された場合は、アプリ終了のタイミングが全てのテストが終わった後になってしまします。でも、今回はそのケースはよしとします。)

TestBase作成

共通部分は基本クラスに押し込むことにします。TestBaseクラスを作成します。テストごとにstaticなインスタンスを持ちたいので、ジェネリックパラメータでテストクラスを指定させるようにします。(タイプが異なるとstaticインスタンスは別に確保される)
AppDriverは継承先でも使うのでprotectedなプロパティーにしておきます。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using System.Collections.Generic;
using EmployeeManagement;
using System.Linq;

namespace TestScenario
{
    publicclass TestBase<T>
    {
        staticprotected AppDriver App { get; set; }
        static Dictionary<string, bool> _tests;
        public TestContext TestContext { get; set; }

        publicstaticvoid NotifyClassInitialize()
        {
            App = new AppDriver();
            _tests = typeof(T).GetMethods().Where(e => 0< e.GetCustomAttributes(typeof(TestMethodAttribute), true).Length).ToDictionary(e => e.Name, e => true);
        }

        publicstaticvoid NotifyClassCleanup()
        {
            App.EndProcess();
        }

        publicvoid NotifyTestInitialize()
        {
            App.Attach();
        }

        publicvoid NotifyTestCleanup()
        {
            if (TestContext.DataRow == null ||
                ReferenceEquals(TestContext.DataRow, TestContext.DataRow.Table.Rows[TestContext.DataRow.Table.Rows.Count - 1]))
            {
                _tests.Remove(TestContext.TestName);
            }
            App.Release(TestContext.CurrentTestOutcome == UnitTestOutcome.Passed && 0< _tests.Count);
        }
    }
}

これをAdjustDriverに継承させます。ライフ管理の部分は随分スッキリしました。

[TestClass]
publicclass AdjustDriver : TestBase<AdjustDriver>
{
    [ClassInitialize]
    publicstaticvoid ClassInitialize(TestContext c)
    {
        NotifyClassInitialize();
    }

    [ClassCleanup]
    publicstaticvoid ClassCleanup()
    {
        NotifyClassCleanup();
    }

    [TestInitialize]
    publicvoid TestInitialize()
    {
        NotifyTestInitialize();
    }

    [TestCleanup]
    publicvoid TestCleanup()
    {
        NotifyTestCleanup();
    }

パラメタライズドテスト考慮

そして、もう一つこのTestBaseに機能を入れます。それはパラメタライズドテスト用です。VSTestのパラメタライズは癖があって、DBかExcelからパラメータを読み込ませることになっています。TestContext.DataRowに現在の行が入ります。このままだと、使いづらいので便利機能を付けておきます。指定の型に変換するメソッドです。

public Data GetParam<Data>() where Data : new()
{
    Data data = new Data();
    foreach(var e intypeof(Data).GetProperties())
    {
        e.GetSetMethod().Invoke(data, newobject[] { Convert(e.PropertyType, TestContext.DataRow[e.Name]) });
    }
    return data;
}

staticobject Convert(Type type, object obj)
{
    //一旦int,bool,stringで//必要に応じて変換方法を追加stringvalue = obj == null ? string.Empty : obj.ToString();
    if (type == typeof(int))
    {
        returnint.Parse(value);
    }
    elseif (type == typeof(bool))
    {
        returnstring.Compare(value, true.ToString(), true) == 0;
    }
    elseif (type == typeof(string))
    {
        returnvalue;
    }
    thrownew NotSupportedException();
}

使う側ではこんな感じで使うことを想定しています。
こんなExcelがあって、(セルは全て文字列型にしています。)
f:id:ishikawa-tatsuya:20150426100803p:plain

読み込みコードです。

//Excelのカラム名称と合わせるclass Data
{
    publicstring Input { get; set; }
    publicint Expectation { get; set; }
}
//ファイル名とシート名を合わせる
[DataSource("System.Data.OleDB",
    @"Provider=Microsoft.ACE.OLEDB.12.0; Data Source=TestParams.xlsx; Extended Properties='Excel 12.0;HDR=yes';",
    "DataSheet$",
    DataAccessMethod.Sequential
)]     
[TestMethod]
publicvoid ParameterizedTest()
{
    var data = GetParam<Data>();
}

システムテストだと多くの人と内容を共有する必要もあって、Excelでパラメータ管理する方が受けが良いですね。ソースにパラメータ埋め込んでいいとかだと、@neueccさんのChainingAssertionを使えばVSTestでも気軽にパラメタライズできます。好みに合わせて使ってみてください。
Chaining Assertion - Home

共通化まで終えたコードはこちらに置きました。次回はこれを使ってシナリオを実装します。
HandsOn14-2/Project/TestScenario at master · Ishikawa-Tatsuya/HandsOn14-2 · GitHub

サンプルコード修正

4/29 TestCleanup、NotifyTestCleanupのコードを修正しました。

Friendly ハンズオン 14-2 テストシナリオ実装

$
0
0

今回はテストシナリオ実装やってみます。(やっと
これを元に作っていきます。
Ishikawa-Tatsuya/HandsOn14-2 · GitHub

テストシナリオはテストチームの方が書くことを前提としています。若干のプログラム知識とトレーニングで書ける(はず)です。

追加画面のテスト

このクラスは追加画面のテストです。この画面ですね。
f:id:ishikawa-tatsuya:20150425222651p:plain

新しくテストクラスを追加します。
f:id:ishikawa-tatsuya:20150425222211p:plain
f:id:ishikawa-tatsuya:20150425222216p:plain

では、前回作ったTestBaseを継承して、お決まりのコードを書きます。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestScenario
{
    [TestClass]
    publicclass AddTest : TestBase<AddTest>
    {
        [ClassInitialize]
        publicstaticvoid ClassInitialize(TestContext c)
        {
            NotifyClassInitialize();
        }

        [ClassCleanup]
        publicstaticvoid ClassCleanup()
        {
            NotifyClassCleanup();
        }

        [TestInitialize]
        publicvoid TestInitialize()
        {
            NotifyTestInitialize();
        }

        [TestCleanup]
        publicvoid TestCleanup()
        {
            NotifyTestCleanup();
        }
    }
}

正常系

まずは、正常系を書きましょう。前にAppDriverの確認でもちょっと書きましたが、そのままでOKです。
書いたら一回実行してテストが通ることを確認してみてください。

[TestMethod]
publicvoid TestAdd()
{
    var addForm = App.MainForm.ButtonAdd_EmulateClick();
    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    addForm.RadioButtonMan.EmulateCheck();
    addForm.ButtonEntry.EmulateClick();
    Assert.AreEqual("ishikawa-tatsuya(男) Japan", App.MainForm.ListBoxEmployee_GetItemText(0));
}

入力エラー

実はこの画面は、入力が変だとエラーがでます。
基本は以下3パターンです。

f:id:ishikawa-tatsuya:20150426161845p:plain
f:id:ishikawa-tatsuya:20150426161846p:plain
f:id:ishikawa-tatsuya:20150426161847p:plain

で、複数の種類のエラーがあると、上の並びの優先順位でエラーが表示されるというものです。そうすると、以下の組み合わせが考えられますね。こういうの考えるときはExcelが便利だったりします。
f:id:ishikawa-tatsuya:20150426210534p:plain

表形式で表せたら、大体はパラメタライズできます。上のエクセルを使ってやってみましょう。
まずは、上の内容のエクセルをTestScenarioフォルダに作って、ソリューションに取り込みます。エクスプローラ上からD&Dで取り込めます。あ、このエクセルのセルは全部文字列型にしといてくださいね。
f:id:ishikawa-tatsuya:20150426164450p:plain

次にExcelファイルをVisualStuido上で選択し、プロパティーの「出力ディレクトリー」を「新しい場合はコピーする」にします。
f:id:ishikawa-tatsuya:20150426164831p:plain

で、テストシナリオはこれです。ErrorParamのプロパティーはExcelの一行目のカラムと合わせます。

class ErrorParam
{
    publicstring Name { get; set; }
    publicstring Address { get; set; }
    publicbool Checked { get; set; }
    publicstring Message { get; set; }
}

[TestMethod]
[DataSource("System.Data.OleDB",
    @"Provider=Microsoft.ACE.OLEDB.12.0; Data Source=Params.xlsx; Extended Properties='Excel 12.0;HDR=yes';",
    "AddTest$",
    DataAccessMethod.Sequential
)]     
publicvoid TestError()
{
    var param = GetParam<ErrorParam>();
    var addForm = App.MainForm.ButtonAdd_EmulateClick();
    addForm.TextBoxName.EmulateChangeText(param.Name);
    addForm.TextBoxAddress.EmulateChangeText(param.Address);
    if (param.Checked)
    {
        addForm.RadioButtonMan.EmulateCheck();
    }
    Assert.AreEqual(param.Message, addForm.ButtonEntry_EmulateClickAndGetMessage());
    addForm.Close();
}

テストシナリオは一つで、複数のパラメータの確認ができました。パラメータが多いものだと、一回の実行でも手動テストより低コストでテスト実行できることもあります。(まあ、システムテストではパラメタライズあんまりしなくていいようにテスト設計するべきではありますが)

いかがだったでしょうか?

ちょっと難しい?でもほとんど定型化されているので慣れたら専門職プログラマーでなくても書けます。ポイントはここには内部仕様は出てこないようにすることですね。

このテストシナリオまで書いた状態のサンプルをここにおいておりますので、自分で動かしつつ、検索のテスト追加したりで練習してみてください。
Ishikawa-Tatsuya/HandsOn14-3 · GitHub

Friendly.Windows.2.6.0をリリースしました。

$
0
0

タイトルの通りです。
それに伴ってFriendly.Windowsを参照しているdllも更新しています。
Friendly.Windows.Grasp.2.5.1
Friendly.Windows.NativeStandardControls.2.2.3
Friendly.FormsStandardControls.2.3.2
Friendly.WPFStandardControls.1.1.5

内容は不具合修正です・・・

.NetFramework4.0をインストールしていないPCでアタッチできなかった

あれー?テストしてなかったなー。まだXPのサポート切れてなかった頃はやってたんですけどね・・・(でもたまたま)
インストーラ形式からNuget形式に変えたときにデグったようです。

原因は・・・

CLRCreateInstanceですね。ホストAPIなんですけど、.Netの4.0がインストールされていないとMSCorEE.dllに入っていないらしいです。

HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));

修正内容

で、書き換えました。
LoadLibraryとGetProcAddressで取得して使います。
CLRCreateInstanceが取れなければ、CorBindToRuntimeEx使います。
CorBindToRuntimeExは逆に4.0以降は非推奨ですね。

//CLRCreateInstanceは4.0がインストールされていなければ存在しないauto hMod = LoadLibrary(L"MSCorEE.dll");
auto clrCreateInstance = (CLRCreateInstanceFnPtr)GetProcAddress(hMod, "CLRCreateInstance");
if (!clrCreateInstance) {
	typedef HRESULT(__stdcall *CorBindToRuntimeExType)(LPCWSTR pwszVersion, LPCWSTR pwszBuildFlavor, DWORD startupFlags, REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);
	auto corBindToRuntimeEx = (CorBindToRuntimeExType)GetProcAddress(hMod, "CorBindToRuntimeEx");
	if (!corBindToRuntimeEx) {
		return FALSE;
	}
	HRESULT hr = corBindToRuntimeEx(szVersion, nullptr, 0, CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost));
	if (FAILED(hr)) {
		return FALSE;
	}
	return TRUE;
}

あんまり.Net4.0が入っていない環境ってないですけどね・・・。
でも、これで今まで引っかかっていた人、すみませんでした orz

Friendly.WPFStandardControls.1.3.0をリリースしました。

$
0
0

WPF系の機能追加です。

x:Name以外でのコントロールの特定

x:Nameを付けるのは、なんら恥じることはありません。テスタビリティーを向上させるという大義名分があります。付けても使わないようにしたらいいだけです。だいたい、x:Name付けなくてもコードビハインドからコントロール取得する手段ありますしね!

でも・・・。

XAML系のガチな人はx:Nameを付けることを良しとしませんw。(ごもっとも)
でも大丈夫です!前述しましたが、x:Name付けなくてもコントロールを取得する手段はあります。で、Friendlyは通常のプログラムでできることなら何でもプロセス越しにできるのです。だからx:Nameにこだわらなくても、WPFの知識のある人ならいくらでも特定できるのですね。

とは言え、VisualTreeやLogicalTreeを走査するのは面倒なので、そのユーティリティーを追加しました。

サンプルです。あ、DataTemplateの例は、かずき (id:okazuki)さんのブログからパクりました。
f:id:ishikawa-tatsuya:20150505180208p:plain

<Window x:Class="Target.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MainWindow"Height="350"Width="525"><Grid><Grid.Resources><DataTemplate x:Key="personViewTemplate"><StackPanel Orientation="Horizontal"><TextBlock Text="{Binding Name}"/><TextBlock Text="さん " /><TextBlock Text="{Binding Age}"/><TextBlock Text="歳" /></StackPanel></DataTemplate></Grid.Resources><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions><TextBox Text="{Binding Memo}"Grid.Row="0"/><TextBlock Text="{Binding Memo}"Grid.Row="1"/><Button Content="OK"Command="{Binding CommandOK}"Grid.Row="2"/><ListBox ItemsSource="{Binding Persons}"ItemTemplate="{StaticResource personViewTemplate}"Grid.Row="3"/></Grid></Window>

コントロール特定

[TestMethod]
publicvoidコントロール特定サンプル()
{
    AppVar main = _app.Type<Application>().Current.MainWindow;
    var logicalTree = main.LogicalTree();

    var textBox = new WPFTextBox(logicalTree.ByBinding("Memo").ByType<TextBox>().Single());
    var textBlock = new WPFTextBlock(logicalTree.ByBinding("Memo").ByType<TextBlock>().Single());
    var button = new WPFButtonBase(logicalTree.ByBinding("CommandOK").Single());
    var listBox = new WPFListBox(logicalTree.ByBinding("Persons").Single());
}

まずは、画面のロジカルツリーをフラットなコレクションにします。

var logicalTree = main.LogicalTree();

LogicalTree()はAppVarとIAppVarOwnerの拡張メソッドにしています。
戻り値はIWPFDependencyObjectCollection<DependencyObject>です。
ちょっと変な型ですか?
まあvarって書いてください。
ちなみにVisualTree()もあります。

var visualTree = main.VisualTree();

それから、逆方向に手繰ることもできます。

var logicalTree = button.LogicalTree(TreeRunDirection.Ancestors);

次は、Bindingのパスから検索します。純粋に文字列で検索してきます。これも戻り値はIWPFDependencyObjectCollection<DependencyObject>です。
上のXamlを見てもらうと分かるように"Memo"というパスのBindingは2個あります。なので、ここで取得されるコレクションの数は2です。

logicalTree.ByBinding("Memo")

そして、そのコレクションの中からTextBoxで絞り込みます。

.ByType<TextBox>()

そうすると1つに特定できているはずなのでSingleを呼びます。

.Single()

Bindingからの検索時は、どのデータに結びついているのかを指定することができます。

var  button = logicalTree.ByBinding("CommandOK", new ExplicitAppVar(main.Dynamic().DataContext)).Single();

それから、実はGeneric型のByTypeで取得した場合は戻り値のコレクションにその型が反映されます。

IWPFDependencyObjectCollection<TextBox> texs =  collection.ByType<TextBox>();

まだないのですが、将来的には、その型に特化した検索の拡張メソッドとか提供するかもしれないですね。
もちろん、それは誰でも作れるので、それぞれのプロジェクトでも作ってみてください。

ちょっと言い訳

IWPFDependencyObjectCollectionっていう変わったインターフェイスが出てきています。(正確にはCollectionではないのですが、相手プロセス内部のCollectionを操作するので、このサフィックスがついています。)

publicinterface IWPFDependencyObjectCollection<out T> where T : DependencyObject
{
    int Count { get; }
    AppVar this[int index] { get; }
    AppVar Single();
}

なんで素直にIEnumerableじゃないかと言うと、Friendlyは回す系は非推奨なのです。プロセス越しにループを回すと遅いからですね。だからIEnumerableにして通常のループ操作(Linqも含め)が使いやすくなるのはちょっと問題あるので特殊なのを用意しています。後述しますが相手プロセス内でLogicalTree()とか使う場合はIEnumerableが返ってきますのでご安心を。

ItemsControlの場合

ItemsControlとくくりましたが、これは色々です。これは単純にVisualTreeを手繰ってもアイテムは取得できません。と言うのは非表示のものは取得できないからです。可視状態にしてから取得する必要がありますね。

以下のコントロールにはそれぞれアイテム取得メソッドを追加しました。

  • ListBox
  • ListView
  • DataGrid
  • TreeView

これを使って、アイテムを取得します。そのアイテムはIAppVerOwnerを実装していますので、それからVisualTreeを手繰れば目的の要素を取得できます。

[TestMethod]
publicvoid ListBoxとか()
{
    AppVar main = _app.Type<Application>().Current.MainWindow;
    var logicalTree = main.LogicalTree();
    var listBox = new WPFListBox(logicalTree.ByBinding("Persons").Single());

    //アイテム取得//可視状態にしている
    var item = listBox.GetItem(20);

    //NameにバインドされたTextBlockを取得する//ListBoxのアイテムはLogicalTree上には現れない
    var textBlock = new WPFTextBlock(item.VisualTree().ByBinding("Name").Single());
    Assert.AreEqual("U", textBlock.Text);
}

これでも取得できない場合は

それは、まあそれぞれで工夫してもらうと言うことで・・・
DLLインジェクションを使って頑張ってみてください。
相手プロセスの内部で実行すれば、もう普通のプログラムですから。
でも、これも少しだけサポートしました。

DependencyObjectに対してLogicalTree()、VisualTree()
IEnumerableに対してByType()、ByBinding()
の拡張メソッドを用意しました。

もちろん相手プロセスでこれらを使う場合は、先にWPFStandardControlsもインジェクションする必要があります。

[TestMethod]
publicvoid内部からの取得もサポート()
{
    //内部で処理をするための準備
    WPFStandardControls_3_5.Injection(_app);
    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);

    //相手プロセスで取得ロジック実行
    var layout = _app.Type(GetType()).GetLayout(_app.Type<Application>().Current.MainWindow);

    //戻り値に格納されているのでそれを使う
    var textBox = new WPFTextBox(layout.TextBox);
    var textBlock = new WPFTextBlock(layout.TextBlock);
    var button = new WPFButtonBase(layout.Button);
    var listBox = new WPFListBox(layout.ListBox);
}

class Layout
{
    public TextBox TextBox { get; set; }
    public TextBlock TextBlock { get; set; }
    public Button Button { get; set; }
    public ListBox ListBox { get; set; }
}

static Layout GetLayout(Window main)
{
    //通常のプログラムなんで、普通のLinqとかも使えるし、ご自由にどうぞ
    var logicalTree = main.LogicalTree();
    returnnew Layout()
    {
        TextBox = (TextBox)logicalTree.ByBinding("Memo").ByType<TextBox>().Single(),
        TextBlock = (TextBlock)logicalTree.ByBinding("Memo").ByType<TextBlock>().Single(),
        Button = (Button)logicalTree.ByType<Button>().Single(),
        ListBox = (ListBox)logicalTree.ByBinding("Persons").Single()
    };
}

サンプルコードを動かした方が分かりやすいですね

はい、こちらに置いておきました。ダウンロードしてテスト実行してもらえば、感覚的にわかると思います。
Ishikawa-Tatsuya/WPFSearch-Sample · GitHub


dynamicで、あれ?って思うこと

$
0
0

Frienldyはdynamicを使うライブラリです。
で、dynamicって書いててちょっと直感的でない時があるんです。
よく考えると、「そうだよね。」ってなるんですけど、僕もハマったので書いておきます。

引数に渡すと、対象の関数まで動的になる

例えば、こんなメソッドがあったとします。

IEnumerable<int> Func(paramsint[] val)
{
    return val;
}

まずは、普通に使ってみましょう。もちろんコンパイル通ります。

Func(3, 2, 1).Where(e => e == 2);

で、引数にdynamicを混ぜてみます。で、ビルドすると・・・

dynamic val = 0;
Func(3, 2, 1, val).Where(e => e == 2);

No...コンパイルエラーです。
しかも、意味わからん感じです。
f:id:ishikawa-tatsuya:20150516112556p:plain

ちょっとWhereの前の.でインテリセンスを見てみましょう。
f:id:ishikawa-tatsuya:20150516112827p:plain
メソッドが動的になっていますね。

最初は、「なんでやねん!」って思いました。
でもこうならざるを得ないんですよね。

オーバーロードの解決が出来ないから

言われてみると、「確かにね」なんですね。
例えば、以下の二つの関数があるとき・・・

int Func(int val)
{
    return val;
}
string Func(string val)
{
    return val;
}

これは実行時まで戻り値の型が分からないんですね。
だから var って書いてますけど ret は dynamic型 なのです。

void Call(dynamic val)
{
    //実行するまで戻り値わからんやん・・・
    val ret = Func(val);
}

型変換

もう一つハマったのが型変換です。
DynamicObjectと言うのがあって、これを継承したクラスをdynamicに入れると、動的な挙動をコントロールできるというものです。
で、TryConvertをoverrideすれば型変換に関する処理をカスタマイズできます。
例えば、シンプルな例で、intにキャストすると10になるというクラスを書いてみます。

publicclass MyDynamic : DynamicObject
{
    publicoverridebool TryConvert(ConvertBinder binder, outobject result)
    {
        result = (int)10;
        returntrue;
    }
}

これは、もちろん変換できます。

dynamic d = new MyDynamic();
int i = (int)d;
int ii = d;

なのに、関数の引数に渡す時はダメなんですよね。なんでー?

int Func(int val) { return val; }

void Call()
{
    dynamic d = new MyDynamic();
    //例外が発生する・・・//型 'Microsoft.CSharp.RuntimeBinder.RuntimeBinderException'の例外が System.Core.dll で発生しましたが、ユーザー コード内ではハンドルされませんでした
    Func(d);
}

まあ、多分これもオーバーロードと絡んでて、複数の変換の可能性を解決できないからでしょうね。残念・・・

追記

オーバーロードとオーバーライドって、いつもどっちがどっちか分からなくなりますよね・・・

参考書籍

C#のdynamicの本と言えばこれですね。
僕もFriendly実装する時に勉強させていただきました。gihyo.jp

拡張メソッドで、あれ?って思ったこと

$
0
0

あれ?って思ったことシリーズ第二弾。

つい先日、Cで書かれた組み込み機器と通信するプログラムをC#で書きました。
で、テストを書くときに、@neueccさんのChaining Assertionを使いました。
これは、Assert書くのを気持ちよくしてくれるライブラリです。

こんな感じ。

int actual = 0;
            
//普通はこう書くけど
Assert.AreEqual(actual, 0);

//こんな感じで書ける
actual.Is(0);

int以外の場合にコンパイルエラー

冒頭でも書きましたが、その日の僕はCで書かれた組み込み機器と通信するプログラムを作っていました。受け渡しするデータはBYTE、WORD、DWORDなわけですよ。で、C#側でもbyte、ushort、uintって出てくるわけですね。で、それに対して次のようなコードを書くとですね・・・

byte actual = 0;
actual.Is(0);

f:id:ishikawa-tatsuya:20150516181208p:plain
えー、なんでやねん・・・ってことになります。

Chaining Assertionの定義はこんな感じです。

publicstaticvoid Is<T>(this T actual, T expected, string message = "")

これって、第一引数のTが既にbyteだから第二引数の0に関してはbyte扱いしてくれても良さそうですよね。っていうか、これ実は拡張メソッドでなければコンパイル通るんです。

void Func<T>(T t1, T t2) { }

[TestMethod]
void Test()
{
    byte actual = 0;
    //これはOK。ほら、分かってるじゃん!
    Func(actual, 0);
}

でも、拡張にしたらコンパイルエラー

publicstaticclass Ex
{
    publicstaticvoid Func<T>(this T t1, T t2) { }
}

[TestClass]
publicclass ExTest
{
    [TestMethod]
    void Test()
    {
        byte actual = 0;
        //コンパイルエラー//分かっておくれよ・・・
        actual.Func(0);
    }
}

しかも、これint以外は全滅かと思いきや、いけるのもあるw。なんでlongとかいけてるんだろ?
×がついているのはコンパイルエラーになります。

//×byte by = 0;
by.Is(0);

//×short s = 0;
s.Is(0);

//×ushort us = 0;
us.Is(0);

int i = 0;
i.Is(0);

//×uint ui = 0;
ui.Is(0);

long l = 0;
l.Is(0);

//×ulong ul = 0;
ul.Is(0);

float f = 0;
f.Is(0);

double d = 0;
d.Is(0);

decimal dec = 0;
dec.Is(0);

まあ、数値を型にするの色々ルールあるし、なんかあるのかなー。できるだけintにするとかね。

で、新たな拡張作って、回避しました。

まあ、キャストするとかあるんですけど、折角Chaining Assertion使ってるのに、そんなの気持ちよくない。
今回は以下のような拡張メソッド作って回避しました。
拡張メソッドはより適合する方を選択してくれます。

publicstaticclass NumericAssertEx
{
    publicstaticvoid Is(thisbyte actual, int expected, string message = "")
    {
        Assert.IsTrue(byte.MinValue <= expected && expected <= byte.MaxValue);
        Assert.AreEqual(actual, (byte)expected, message);
    }

    publicstaticvoid Is(thisshort actual, int expected, string message = "")
    {
        Assert.IsTrue(short.MinValue <= expected && expected <= short.MaxValue);
        Assert.AreEqual(actual, (short)expected, message);
    }

    publicstaticvoid Is(thisushort actual, int expected, string message = "")
    {
        Assert.IsTrue(ushort.MinValue <= expected && expected <= ushort.MaxValue);
        Assert.AreEqual(actual, (ushort)expected, message);
    }

    publicstaticvoid Is(thisuint actual, int expected, string message = "")
    {
        Assert.IsTrue(0<= expected);
        Assert.AreEqual(actual, (uint)expected, message);
    }

    publicstaticvoid Is(thisulong actual, int expected, string message = "")
    {
        Assert.IsTrue(0<= expected);
        Assert.AreEqual(actual, (ulong)expected, message);
    }
}

Microsoft MVP for .Netを受賞いたしました!

$
0
0

おかげさまで、二回目の受賞となりました。
これも支えてくださった皆様のおかげです。

去年は初年度ということで色々な経験をさせていただきました。
特に印象に残ったのはグローバルサミットと、MVP Showcaseですね。何と2位になりました!
http://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-15-70/4774.MVP_5F00_Global_5F00_Summit_2D00_8889_5B00_1_5D00_.jpg
MVP Showcase Winners!

とは言え、過去の栄光に浸ってる場合じゃない!
どんどん前に進んで行きますよー。

と言うわけで引き続きよろしくお願いします。

Friendly.WPFStandardControls.1.4.0を公開しました。

$
0
0

NuGet Gallery | Friendly.WPFStandardControls 1.6.0
今回は機能追加です。

ボタン検索

前回のバージョンアップでは、DependencyObjectに対して、バインディングとタイプで検索する機能を追加しました。
Friendly.WPFStandardControls.1.3.0をリリースしました。 - ささいなことですが。

これで大体取れるのですが、ボタンはApplicationCommandsを設定することが多く、また画面に複数個あることが普通であったために上記の手法で取れないことがありました。
それでコマンドから検索する機能も共通で定義しました。

例として次の画面でボタンを特定してみます。

<Window x:Class="Target.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MainWindow"Height="115"Width="238"><Grid><Grid.ColumnDefinitions><ColumnDefinition /><ColumnDefinition /></Grid.ColumnDefinitions><Button Content="Close"Command="ApplicationCommands.Close"/><Button Content="A"CommandParameter="A"Grid.Column="1"/></Grid></Window>

f:id:ishikawa-tatsuya:20150720225054p:plain

特定する部分のコードです。

var app = new WindowsAppFriend(Process.Start("Target.exe"));
WindowControl main = WindowControl.FromZTop(app);

//ApplicationCommands.Closeの割り当たっているボタンを特定
var buttonClose = main.LogicalTree().ByType<Button>().ByCommand(ApplicationCommands.Close).Single();

//CommandParameterから特定
var buttonA = main.LogicalTree().ByType<Button>().ByCommandParameter("A").Single();

前回追加したByBining、ByTypeはDependencyObjectに対応するAppVar、IAppVarOwnerに対して使えました。今回のはButtonBaseを継承しオブジェクトに対して使えます。ポイントはByTypeです。

IWPFDependencyObjectCollection<Button> elments = main.LogicalTree().ByType<Button>();

ByCommand、ByCommandParameterはIWPFDependencyObjectCollection<Button>に対する拡張メソッドになっています。

前回同様、対象プロセスの中でも使えます。複雑なことをする場合はDLLインジェクションでコードを送り込んで検索してください。

//インジェクションするコードstatic Button Search(Window window)
{
    var buttons = window.LogicalTree().ByType<Button>();
    var button = buttons.ByCommand(ApplicationCommands.Close).Single();

    //対象が.net3.0アプリの場合は拡張メソッドが使えないのでこちらを利用してください。
    button = ButtonSearcherInTarget.ByCommand(buttons, ApplicationCommands.Close).Single();
    button button;
}

TreeViewItemのヘッダー

TreeViewItemに対応するオブジェクトを取得する手段は今までも提供していました。しかし、TreeViewItemはそれ自体が階層型になっています。そこからさらにコントロールを検索する場合はヘッダー部分のみ検索する必要があります。今回はそれを提供しました。

例えば、こんなツリーがあって、
f:id:ishikawa-tatsuya:20150720230842p:plain
アイテムのボタンをとるならこんな感じ。

WPFTreeView tree = new WPFTreeView(window.LogicalTree().ByBinding("TreeData"));
var item = tree.GetItem(1);
//ヘッダー部分を取得
var header = item.HeaderContent;
//そのビジュアルツリーから目的のコントロールを取得
var button = new WPFButtonBase(header.VisualTree().ByType<Button>().Single());
button.EmulateClick();

ご要望あれば連絡ください。

その他、「これも検索方法を提供して欲しいんだけど」みたいなのがあれば連絡いただければ対応できるかもしれません。もし共通化するのは難しかったとしても、Friendlyの基本機能を使えば、ほとんどは検索可能なのでアドバイスくらいはできます。(x:Name使えばいいじゃんとかw)
よろしくお願いします!

Infragistics社、GrapeCity社のコントロール操作ライブラリの作成を始めました。

$
0
0

Friendlyの便利なところは、通常のAPIをRPCでプロセス越しに呼び出せること、DLLインジェクションでテスト時だけ対象プロセスにコードを注入できることです。
この機能を使えば、サードパーティー製のコントロールも簡単に操作できます。
と言うわけで、InfaragisticsさんとGrapeCityさんの製品の操作ライブラリを作ってみました。

対象の製品は当然有料なんですが、InfragisticsさんはMVPにライセンス提供してくれているのと、GrapeCityさんは、これ作るって言っていったら特別にライセンスを提供してくれました。両社様ありがとうございます!

もちろん今回作った操作ライブラリは無料ですよ。お気軽にご利用ください。

ライブラリ

Friendly.XamControls

GitHub, NuGet
XamCalendarDriver
XamDataGridDriver
XamDataTreeDriver
XamGridDriver
XamOutlookBarDriver
XamRibbonDriver
XamTabControlDriver

Friendly.FarPoint

GitHub, NuGet
FpSpreadDriver

FriendlyC1.Win

GitHub, NuGet
C1FlexGridDriver

Nugetにα版で公開しています。ソースコードも公開していますのでよかったら見てください。全てテストコードが付いていて、それがサンプルになっています。
実はちょっと実装方法を工夫していて、操作ライブラリはコントロールのアセンブリ(FarPoint.Win.dll、InfragisticsWPF4.v14.2.dllなど)を参照しない作りになっています。
なので、操作ライブラリはライセンスなしで使えます。それから、対象の詳細なバージョンに依存せずに使えます。(多分

協力者求む!

現在作っている操作クラスは、弊社が自動化支援する中で使う必要ができたものです。(お客様のアプリが使っているコントロールですね)

で、協力者募集中です。「このコントロールも対応して欲しい!」って思った方、良かったら一緒につくりませんか?作り方は簡単ですよー。
ていうか、中の人どうですかw

内容は・・・

詳細な内容は、このブログでも少しづつ紹介していきます。乞うご期待!

Viewing all 104 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>