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

ささいなことですが。

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

Friendlyハンズオン 5.DLLインジェクション

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

引き続き、詳細なドキュメントは公式ページも参照しながらでお願いします。
テスト自動化 Friendly - 株式会社Codeer (コーディア)

はい、昨日までで、内部API呼び出しは全部やったと思います。(多分
今日は、Friendlyのもう一つの大技、DLLインジェクションに関してやってみます。

DLLインジェクションとは

DLLインジェクションという技術自体はWindowsAPIで実現できます。
次のどちらかですね。
・Createremotethread + LoadLibrary
・グローバルフック

でも、これは高等技術です。
面倒なコードを書く必要があります。
あと、ネイティブのDLLでないとインジェクションできません。

FriendlyのDLLインジェクション

FriendlyのDLLインジェクションは、それに比べると超簡単です。
しかも、ネイティブのDLLはもちろん、.Netのアセンブリまでインジェクションできちゃうんです。
早速、やってみましょう。

[TestMethod]
public void DLLインジェクションでボタン追加()
{
    //たったの一行でインジェクション
    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);
    //ボタン追加処理を相手プロセスで実行する
    _app.Type(GetType()).AddButtonInTarget();
    //確認
    dynamic form = _app.Type<Application>().OpenForms[0];
    Assert.AreEqual("追加したボタン", (string)form.Controls[0].Text);
}

static void AddButtonInTarget()
{
    var form = Application.OpenForms[0];
    form.Controls.Add(new Button() { Text = "追加したボタン" });
}

ブレイクを設定してデバッグ実行
f:id:ishikawa-tatsuya:20141211235756p:plain
こんな簡単にアセンブリをインジェクションできるんですね。びっくりです。
もちろん、インジェクションしただけでは、意味ないので、その下の行でインジェクションしたメソッドを呼び出しています。
で、ここで.Netに詳しい人は「プロービングにないアセンブリを読み込ませても・・・」みたいなことを考えるかもしれせん。
でも、大丈夫です。Frinedlyはアセンブリをインジェクションした場合、AppDomain.AssemblyResolveを使って型の解決をします。

メリット

アトミックに処理を実行できる

実はこれ、この間やった以下のものと実行結果は同じです。
何が違うのでしょうか?

[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);
}

それは、処理の実行される間隔です。
このコードだと
・フォームを取得
・ボタンを生成
・フォームに追加
のそれぞれの間に割り込む隙があります。
対してDLLインジェクションの方はその間に隙がありません。
隙とは具体的にはメッセージループが回る可能性のことです。
まあ、ボタンを追加とかならいいのですが、例えば、グリッドとかツリーで「フォーカスを当てる」「編集状態にする」「文字列変更」「編集完了」の一連の流れに万が一割り込まれた場合に、結果が変わるような処理はアトミックに実行したいですね。

高速に処理を実行できる

前にも書きましたが、Friendlyの呼び出しはプロセス間通信が実施されるので低速です。
普通に呼び出す分には特に問題ないのですが、大量にループを回すような処理を書くには向いていません。
その場合は、ループを回す処理自体を対象プロセスにインジェクションして実行させるようにしましょう。
えーと、LINQが嫌いとかではないですよ。
ループを回すサンプルだからこんな書き方しただけです。

[TestMethod]
public void DLLインジェクションで大容量データ対応()
{
    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);
    //高速に実行できる
    dynamic arrayInTarget = _app.Type(GetType()).MakeArray();
    Assert.AreEqual(10000, (int)arrayInTarget.Length);
}

static int[] MakeArray()
{
    List<int> l = new List<int>();
    for (int i = 0; i < 10000; i++)
    {
        l.Add(i);
    }
    return l.ToArray();
}
型安全にコードを書ける

インジェクションするコードは普通に書けば良いので、もちろん型安全です。
(Friendlyの呼び出しはdynamicを使うのでダックタイピングです)

ネイティブDLL公開関数はこれでないと無理

FriendlyはネイティブのDLL公開関数を実行できると言いましたが、それは実はネイティブのDLL公開関数を.Netのstaticメソッドに変換して呼び出せるということなのです。

[TestMethod]
public void TestRect()
{
    dynamic form = _app.Type<Application>().OpenForms[0];

    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);
            
    //User32のDLL公開関数を実行させる
    _app.Type(GetType()).MoveWindow(form.Handle, 0, 0, 200, 200, true);

    Assert.AreEqual(0, (int)form.Left);
    Assert.AreEqual(0, (int)form.Top);
    Assert.AreEqual(200, (int)form.Right);
    Assert.AreEqual(200, (int)form.Bottom);
}

[DllImport("User32.dll")]
static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);

まあ、MoveWindowならわざわざ相手プロセスで実行せずともプロセス越しに実行可能なのですが、サンプルということで許してください。
ともあれ、この呼び出しなら相手プロセス内でMoveWindow(ネイティブDLL公開関数)が実行されています。
もう少し詳細をこちらに書いています。
ネイティブDLL公開関数の呼び出し - 株式会社Codeer (コーディア)

デメリット

デバッグが難しい

インジェクションするコードのデバッグは普通にはできません。相手プロセスにアタッチすると可能ですが手間です。

相手プロセスの生存期間中は再ビルドできない

インジェクションしたコードを持つexeの生存期間中にビルドをすると、dllを削除できずにビルドエラーになります。

テストシナリオ以外で使おう

このデメリットのため、テストシナリオで使うのはお勧めできません。
では、使いどころはどこかというと
・コントロール操作のラッパー
・アプリケーションドライバ

コントロール操作のラッパーはFriendlyの上位ライブラリとかです。
NuGet Gallery | Friendly.Windows.NativeStandardControls 2.1.7
NuGet Gallery | Friendly.FormsStandardControls 2.2.2
NuGet Gallery | Friendly.WPFStandardControls 1.1.2
おそらく、たいていのプロジェクトは各自いくつかのカスタムコントロールを持っていると思うので、その操作クラスを作るときです。

アプリケーションドライバは、テストを記述するときの「操作の層」になります。
Friendlyでテストを作る場合は、この設計パターンを使うことを推奨しています。
私もこの資料で少し説明しています。
【SQiP2014】システム操作インターフェイス最適化によるテスト自動化ROI向上

元々は継続的デリバリーという本で紹介されている手法です。
Amazon.co.jp: 継続的デリバリー 信頼できるソフトウェアリリースのためのビルド・テスト・デプロイメントの自動化: David Farley, Jez Humble, 和智 右桂, 高木 正弘: 本

明日は

なんと、Friendlyアドベントカレンダーで初めて僕以外の人が書いてくれます。
関西が誇るフルスタックエンジニア@fuku518さんです。