Codeer Friendlyなら何でもできる!

Friendly 友達だから〜♪
 Friendly 何でもできる〜♪
  それは夢のライブラリ〜♪
  (ライブラリ〜)

  あなたの自動化助けま〜す〜♪

今日はWPFの話でもASP.NETの話でもJavaScriptの話でもありません。
世界一のテスト自動化ライブラリー Codeer Friendlyのお話です。

そう今日の記事はFriendly Advent Calendar 2014 13日目のエントリーなのです。

Friendly Advent Calendar 2014 - Qiita

まずはCodeer Friendlyをご存じない方は
概要 - 株式会社Codeer (コーディア)
をご覧ください。
で、見てもあんまりなんかわからんかったという方も、そんなん全然知ってるわと言う方も、そうじゃない方もしばしお付き合いを。
最後まで読んでもやっぱり「ふーん」だった方は来週もう一回 Friendly Advent Calendar 2014 に投稿しますので、それを見てからご判断ください!

でっかい声で言いたいのは、マジでCodeer Friendlyは凄いです!
で、凄さは結構簡単に伝わります。だけど。。。

ほんで、でっかい声では言えないけれど小さい声では聞こえない話として
いろんな意味でとっつきにくいのです。(最近かなりその点は改善されつつあって素晴らしいと思います!マジで)
非常識なことが簡単にできるが故に、根本的な頭の切り替えが必要になるというジレンマっつーかにわたまって言うか。

使う側から見たFriendlyの良いところ

  • 別プロセスから操作するのに圧倒的に高速である
  • 設計者の石川さん(http://ishikawa-tatsuya.hatenablog.com/)の知見が盛り込まれた安定かつ確実なUI操作
  • 変数のスコープもマーシャリングも意識せずにテスト対象の中身にガンガンアクセスできる
  • 言わずもがなのすげーことがさらりとできる超絶マジック

ぶっちゃけFriendlyのとっつきにくいところ

  • ライブラリーがどれが必要なのかわからない(Friendly/Windows/Grasp/etc)
  • シンタックスが直観的じゃない(覚えるべきお作法が多い)
  • エラーの意味がわかりにくい(やってることが凄いんで仕方ないのだけれど)
  • 実行時でないとエラーにならない(根っからのC#使いの人には最初厳しい)
  • 書いてるコードが相手プロセス中で実行されるってのが意味不明(言葉の通りなんだぜッ)

個人的にはdynamic型を駆使した動的な型を最大限に利用するFriendlyの書き方に苦労しました。
たまたま石川さん自身から手とり足とりレクチャーしていただいたおかげでこうやってAdvent Calendarを書けるほどになりました。

今回私が書く内容は、自分(自動テストだったり、Friendlyを使う側のコード)が定義した処理を相手プロセス内で実行するってことなんですけど、よく見たら昨日のFriendly Advent Calendarの内容と同じですね。アハハ、すげーシンクロニシティ
Friendlyハンズオン 5.DLLインジェクション - ささいなことですが。

実際のコードを示します。

        private void OnButton1(object sender, RoutedEventArgs e)
        {
            var process = Process.Start("WpfApplication1.exe");//(1)
            var app = new WindowsAppFriend(process);//(2)
            WindowsAppExpander.LoadAssembly(app, GetType().Assembly);//(3)
            app.Type(GetType()).StaticProcedure();//(4)
            app.Dispose();//(5)
        }

        private static void StaticProcedure()
        {
            MessageBox.Show("相手のプロセスで生成されたメッセージボックス");
        }

一番のポイントは(4)のapp.Type(GetType())と(3)の行です。
で、これが最初はハードルが高い。まあ、定型句として「こう書くもんや」と意味はともかくまずは慣れるというのも1つの手ではあるんですが。
何が起こるのかと言えばapp.Type(GetType())の後に自分が定義したstatic メソッドを続けて書くと、相手プロセス内にそのstaticメソッドが送り込まれて相手側で実行される。というだけです。

ほんで、これをコンパイルエラーが出ない程度に間違えて記述してみるとどうなるか見てみましょう。
app.Type(GetType())を間違えてapp.Type()と書いてしまったとしましょう。
コンパイルエラーは出ないので、そのまま実行してみます。

ギャーース!
不正なstatic呼び出しなのかー操作情報には型が必要なのかー

なるほど、型が必要なのか!とポンと膝を打ち今度は
app.Type()を間違えてapp.Type()と書いてしまったとしましょう。
これまたコンパイルエラーは出ないので、そのまま実行してみます。

ギャーース!またエラー
指定の操作が見つからないのかー

・・・と、私も最初はハードルの高さに挫折しそうになりました。
しかもこの辺のお作法と実行時にしかエラーが出ないのはやってることの凄さとのトレードオフなのでいかんともしがたいところではあるのです。
なので、私はfool proofになるように1つヘルパメソッドを追加しました。
その名もGetStaticMethodInvoker(ベタなネーミング!)

public partial class MainWindow : Window
{
    private WindowsAppFriend _app;

    private void OnButton1(object sender, RoutedEventArgs e)
    {
        var process = Process.Start("WpfApplication1.exe");
        _app = new WindowsAppFriend(process);
        WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);
        GetStaticMethodInvoker().StaticProcedure();
        _app.Dispose();
    }

    private dynamic GetStaticMethodInvoker()
    {
        return _app.Type(GetType());
    }

    private static void StaticProcedure()
    {
        MessageBox.Show("相手のプロセスで生成されたメッセージボックス");
    }

    public MainWindow()
    {
        InitializeComponent();
    }
}

これで私にとっての直観的なロジックと関心ごとの分離が実現できました。
これを使って、相手のプロセスにstatic メソッドを送り込むことで

Friendly 友達だから〜♪
 Friendly 何でもできる〜♪

何でもできるで遊んでみたいと思います。
1つのまあまあ巨大なstatic メソッドを作成します。Friendlyの挙動の説明に使いたいので、ワンライナー風に且つ手なりのベタなコードになってます。

private static void StaticProcedure(Window window)
{
    const int horzCount = 8 + 2;
    const int vertCount = 6 + 2;
    const int bombCount = 15;
    const int cellSize = 50;

    window.Width = cellSize * (horzCount - 2) + 20;
    window.Height = cellSize * (vertCount - 2) + 60;

    var wrap = new WrapPanel
    {
        Margin = new Thickness(1, 1, 0, 0)
    };
    var rootGrid = window.Content as Grid;
    rootGrid.Children.Clear();
    rootGrid.Children.Add(wrap);

    var opened = new bool[horzCount * vertCount];
    var bombed = new int[horzCount * vertCount];
    var counts = new int[horzCount * vertCount];

    var rnd = new Random(Environment.TickCount);
    //●〜*配置
    for (var i = 0; i < bombCount; i++)
    {
        var h = rnd.Next(horzCount - 2) + 1;
        var v = rnd.Next(vertCount - 2) + 1;
        bombed[h + v * horzCount] = 1;
    }
    //●〜*数カウント
    for (var y = 1; y < vertCount - 1; y++)
    {
        for (var x = 1; x < horzCount - 1; x++)
        {
            var index = x + y * horzCount;
            counts[index] = bombed[index - horzCount - 1] + bombed[index - horzCount - 0] + bombed[index - horzCount + 1] +
                            bombed[index - 0 - 1] + bombed[index - 0 - 0] + bombed[index - 0 + 1] +
                            bombed[index + horzCount - 1] + bombed[index + horzCount - 0] + bombed[index + horzCount + 1];
        }
    }
    //フィールド生成&マウスイベント処理
    for (var y = 1; y < vertCount - 1; y++)
    {
        for (var x = 1; x < horzCount - 1; x++)
        {
            var index = x + y*horzCount;
            var cell = new Border
            {
                BorderBrush = new SolidColorBrush(Colors.Black),
                BorderThickness = new Thickness(1),
                Width = cellSize,
                Height = cellSize,
                Margin = new Thickness(-1, -1, 0, 0),
                Tag = index
            };
            var grid = new Grid();
            var text = new TextBlock
            {
                FontSize = 32,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            };
            text.Text = (bombed[index] == 1) ? "*" : counts[index].ToString();
            text.Foreground = (bombed[index] == 1)
                ? new SolidColorBrush(Colors.Crimson)
                : new SolidColorBrush(Colors.DodgerBlue);
            grid.Children.Add(text);
            var cover = new Grid
            {
                Background = new SolidColorBrush(Colors.LightSkyBlue),
                Opacity = 1
            };
            grid.Children.Add(cover);
            cell.Child = grid;
            wrap.Children.Add(cell);
            cell.MouseDown += (o, args) =>
            {
                //マウスイベント
                var cell2 = o as Border;
                var index2 = cell2.Tag as int?;
                if (index2 == null) return;

                var grid2 = cell2.Child as Grid;
                var cover2 = grid2.Children[1] as Grid;

                if (opened[(int) index2]) return;

                if (args.ChangedButton == MouseButton.Right)
                {
                    //右クリック
                    cover.Background = new SolidColorBrush(Colors.DarkOrange);
                }
                else
                {
                    //左クリック
                    cover2.Opacity = 0;
                    opened[(int) index2] = true;
                    if (bombed[(int) index2] == 1)
                    {
                        //全部開ける
                        for (var y3 = 0; y3 < vertCount - 2; y3++)
                        {
                            for (var x3 = 0; x3 < horzCount - 2; x3++)
                            {
                                var index3 = x3 + y3*(horzCount - 2);
                                var index4 = x3 + 1 + (y3 + 1)*horzCount;
                                var cell3 = wrap.Children[index3] as Border;
                                var grid3 = cell3.Child as Grid;
                                var cover3 = grid3.Children[1] as Grid;
                                if (bombed[index4] == 1)
                                {
                                    cover3.Opacity = 0;
                                }
                            }
                        }
                        MessageBox.Show(window, "残念!踏んじゃった。");
                    }
                    else
                    {
                        var restCount = 0;
                        for (var y4 = 1; y4 < vertCount - 1; y4++)
                        {
                            for (var x4 = 1; x4 < horzCount - 1; x4++)
                            {
                                var index4 = x4 + y4*horzCount;
                                if (opened[index4] == false && bombed[index4] == 0) restCount++;
                            }
                        }
                        if (restCount == 0)
                        {
                            for (var y3 = 0; y3 < vertCount - 2; y3++)
                            {
                                for (var x3 = 0; x3 < horzCount - 2; x3++)
                                {
                                    var index3 = x3 + y3*(horzCount - 2);
                                    var cell3 = wrap.Children[index3] as Border;
                                    var grid3 = cell3.Child as Grid;
                                    var cover3 = grid3.Children[1] as Grid;
                                    cover3.Opacity = 0;
                                }
                            }
                            MessageBox.Show(window, "やったね、大成功!");
                        }
                    }
                }
            };
        }
    }
}

Visual Studioで1つWPFアプリケーションのソリューションを作って、その中にもう1つWPFアプリケーションのプロジェクトを追加します。

↑こんな感じ。
で、後から追加したWpfApplication1 の方はC#のWPFアプリケーションプロジェクトのスケルトンのままで。
Friendly側のプロジェクトから、上の巨大なstatic メソッドを実行させれば空っぽのWPFアプリケーションが実際に動作するプログラムに動的に変化します。

何でもできるってのはこういうことやと思いますよね!
今回のネタの全ソースをGitHubにあげてますんで、良かったら遊んでみてね!
GitHub - fuku518/FriendlyDemo20141213

 

明日のFriendly Advent Calendarは、現実世界の何でもできるエンジニア ぽざうね (id:posaunehm)さんです!
テストも自動化も難易度の高い技術も大好きぽざうねさんとFriendlyの相性は抜群だと思います。