ささいなことですが。

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

Friendlyハンズオン 3.操作対象プロセスに内部APIを実行させる

これは「Friendly Advent Calendar 2014 - Qiita」の記事です。

引き続き、詳細なドキュメントは公式ページも参照しながらでお願いします。
テスト自動化 Friendly - 株式会社Codeer (コーディア)
それから、以前に書いたこの記事も参考になります。
Friendly 操作対象プロセス内のインスタンス - ささいなことですが。

今日やること

1.自分で作ったstaticメソッドの呼び出し
2.相手プロセスのフォームのプロパティーを変更
3.相手プロセスのフォームにボタンを追加

ではいよいよ、相手プロセスを操作していきます。

自分で作ったstaticメソッドの呼び出し

まずは、staticなメソッドを実行させてみます。
自分で作ったstaticのメソッドを呼び出してみましょう。

Target/TargetForm.cs
メソッドを付け足します。

public static int MyStaticMethod()
{
    return 100;
}

単に100返すだけのメソッドですね。
これを実行させてみます。

では、操作する側の Test/TargetFormTest.cs です。
まずは、これを付け足してください。
using Codeer.Friendly.Dynamic;

あ、それから、Testメソッド名称がデフォルトのままでしたね。
消して、これに変えておきます。

[TestMethod]
public void プロセス越しに自分で作ったstaticメソッド呼び出し()
{
    int value = _app.Type("Target.TargetForm").MyStaticMethod();
    Assert.AreEqual(100, value);
}

では、実行してみます・・・。
成功しましたよね?
信じられないかもしれませんが、これはテストプロセスで実行されたのではなく、相手プロセスで実行されてその戻り値を取得したのです。
_appは相手プロセスと繋げる能力を持っています。

相手プロセスのフォームのプロパティーを変更

もうちょっとやります。
今度は相手プロセスのフォームを取ってきて、そのプロパティーを変更してみましょう。
その前に、TestプロジェクトはSystem.Windows.Formsへの参照が設定されていませんね。
WinFormsのアプリをテストする場合はあった方がやりやすいので付けておきます。

ソリューションエクスプローラの参照設定を右クリック→参照の追加を選択
f:id:ishikawa-tatsuya:20141209232949p:plain

アセンブリフレームワークを選択し、System.Windows.Formsにチェックを入れOK
f:id:ishikawa-tatsuya:20141209233009p:plain
f:id:ishikawa-tatsuya:20141209233022p:plain

まずは、タイトルを変えてみます。
このメソッドを追加してください。

[TestMethod]
public void TargetFormのタイトルを変える()
{
    dynamic form = _app.Type<System.Windows.Forms.Application>().OpenForms[0];
    form.Text = "Friendly!";
    Assert.AreEqual("Friendly!", (string)form.Text);
}

今度は、.NetのクラスSystem.Windows.Forms.ApplicationのOpenFormsプロパティーを呼び出していますね。
この時、相手プロセスで開いているフォームは一つなので、それが取得できます。
ここで取得した form は相手プロセス内の参照と繋がっています。
なので、これに対するAPIの呼び出しは全て相手プロセスのメインスレッドで実行されます。
あと、ポイントは

Assert.AreEqual("Friendly!", (string)form.Text);

の(string)でのキャストですね。
form.Textの時点では、Textもまだ相手プロセス内にあるので、比較できないのですね。
stringにキャストすることによって、テストプロセスにコピーされ、文字列として使用可能になるのです。

これは、ブレイクを設定して途中で止めて見てみましょう。
(止めないと速すぎて見えないですよね)
Assertの行に設定してください。
止まった時に文字列が Friendly! に変わっているのが確認できますね。
f:id:ishikawa-tatsuya:20141209233058p:plain

次は色も変えてみます。
おっと、Colorを使うので、TestプロジェクトにSystem.Drawingへの参照を足します。
f:id:ishikawa-tatsuya:20141209233612p:plain

コードはこれです。
ついでにネームスペースをusingしておきす。

using System.Drawing;
using System.Windows.Forms;
[TestMethod]
public void TargetFormの背景色を変える()
{
    dynamic form = _app.Type<Application>().OpenForms[0];
    form.BackColor = Color.Blue;
    Assert.AreEqual(Color.Blue, (Color)form.BackColor);
}

これもブレイクを設定してデバッグ実行で確認してみましょう。
f:id:ishikawa-tatsuya:20141209233746p:plain

相手プロセスのフォームにボタンを追加

はい、ここまででプロパティーに渡したものは、シリアライズできるものでした。
Friendlyを使ったプロパティー、フィールドへのセットや、メソッドの引数にはシリアライズ可能なものと、相手プロセスの参照が渡せます。
Colorやstringはシリアライズ可能ですね。
では、相手プロセスの参照って何でしょうか?
ここまでで出てきたものだと form がそうですね。
あと、form.Text、form.BackColorも実は、この時点では相手プロセスの参照です。
(string)や(Color)でキャストすると、テストプロセスへコピーが転送されてくるのです。

つまり、Friendly越しにプロパティーやフィールドで取得したもの、それからメソッドの戻り値が、相手プロセスの参照になります。これはポイントです。
Friendlyはあまりに自然に相手プロセスと繋がるため、たまに混乱する人もいます。

相手プロセスの参照を渡すサンプルも書いてみます。
ボタンを追加してみます。

[TestMethod]
public void TargetFormにボタンを追加する()
{
    dynamic form = _app.Type<Application>().OpenForms[0];
    dynamic button = _app.Type<Button>()();
    button.Text = "追加したボタン";
    form.Controls.Add(button);
    Assert.AreEqual("追加したボタン", (string)form.Controls[0].Text);
}

ポイントは、ボタンの生成とその追加ですね。
_app.Typeってstaticメソッド呼び出しでも使いましたね。
これには、相手プロセスでstaticな操作を実行させる機能とインスタンスを生成する機能があります。
f:id:ishikawa-tatsuya:20141209235546p:plain

で、form.Controls.Add(button); では引数に相手プロセスに生成したボタンを渡していますね。
この流れではbuttonの実体はずっと相手プロセスにあります。
一度もテストプロセスには来ていません。
では、これも実行します。
f:id:ishikawa-tatsuya:20141209234036p:plain

どうだったでしょうか?
Friendly自体の覚えることは非常に少なくてシンプルなのですが、普段あまりやらない頭の使い方をしますね。
自分でも、TargetFormに色々メソッド足して、それをTestから呼び出してみるともっと理解が進むと思います。

今日のコードの全文を書いておきます。

Target/TargetForm.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Target
{
    public partial class TargetForm : Form
    {
        public TargetForm()
        {
            InitializeComponent();
        }

        public static int MyStaticMethod()
        {
            return 100;
        }
    }
}

Test/TargetFormTest.cs

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics;
using System.Threading;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Dynamic;
using System.Drawing;
using System.Windows.Forms;

namespace Test
{
    [TestClass]
    public class TargetFormTest
    {
        WindowsAppFriend _app;

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

        [TestCleanup]
        public void TestCleanup()
        {
            var process = Process.GetProcessById(_app.ProcessId);
            process.CloseMainWindow();
            process.WaitForExit();
        }

        [TestMethod]
        public void プロセス越しに自分で作ったstaticメソッド呼び出し()
        {
            int value = _app.Type("Target.TargetForm").MyStaticMethod();
            Assert.AreEqual(100, value);
        }

        [TestMethod]
        public void TargetFormのタイトルを変える()
        {
            dynamic form = _app.Type<System.Windows.Forms.Application>().OpenForms[0];
            form.Text = "Friendly!";
            Assert.AreEqual("Friendly!", (string)form.Text);
        }

        [TestMethod]
        public void TargetFormの背景色を変える()
        {
            dynamic form = _app.Type<Application>().OpenForms[0];
            form.BackColor = Color.Blue;
            Assert.AreEqual(Color.Blue, (Color)form.BackColor);
        }

        [TestMethod]
        public void TargetFormにボタンを追加する()
        {
            dynamic form = _app.Type<Application>().OpenForms[0];
            dynamic button = _app.Type<Button>()();
            button.Text = "追加したボタン";
            form.Controls.Add(button);
            Assert.AreEqual("追加したボタン", (string)form.Controls[0].Text);
        }
    }
}

明日へ続く・・・