岡嶋 大介
ObjectPeepHoleライブラリを使うと、任意の.NETのオブジェクトについて、
このときあなたがしなければならないことは、所定のルールに従ってインタフェースを書くだけです。そのインタフェースにあわせて実装を実行時に生成するのがこのライブラリのミソです。
主にNUnitなどの単体テストのテストコードを書く支援を想定しています。
本来はprivate/protectedなフィールドやメソッドを、テストケースで呼び出す必要があるというだけのためにやむをえずinternalやpublicにすることはよくあります。ObjectPeepHoleを使うとprivate/protectedなものに直接アクセスできるので、そのような回避をする必要がありません。
より重要なのはイベントの発行です。たとえばGUIアプリケーションを単体テストするためには、キーボードやマウス関係のイベントを自在に発行できなければ不便で仕方ないですが、.NET Framework単独ではそれはできません。(少なくともMicrosoftの実装ではそうです。Monoは調べてません)
ObjectPeepHoleを使うとインタフェースに所定の名前でメソッドを書くだけでイベントの発行ができます(実はこれがObjectPeepHoleを作成した最初の動機です)。.NETにはPerformClick()メソッドなど、一部のイベントについては苦し紛れにイベント発行用の機能がついていたりしますが、このObjectPeepHoleを使うとWinFormsのほぼすべてのクラスとイベントが一挙にカバーできます。
フィールドやメソッドのアクセスは、FieldInfo.GetValue()やMethodInfo.Invoke()を使えばObjectPeepHoleでなくてもたしかに可能ですが、これはインタフェースを書くだけでよいので、単体テストのコードを書くときにふつうのクラスであるかのように扱えますし、IntelliSenseの恩恵をフルに受けることができる点で優れています。
使い方の質問、機能追加の要望等はsourceforgeのプロジェクトページの各機能を使っていただくか、okajima@lagarto.co.jp(@はスパムよけのため全角になってます)までメールにてご連絡ください。
ちなみに、ObjectPeepHoleは私が開発した株式取引用プラットフォームTacticoのGUIテストを自動化する方法を考えているうちに思いついたものです。
ターゲットになるクラス1つにつきインタフェースを1つ書きます。
例えばこんなクラス Foo があるとして、単体テスト用にprivateなもののアクセスやイベント発行がしたいとします。
class Foo {
private string _name;
private int SomeMethod(string a) {
...
}
public EventHandler SomeEvent;
}
それに対し、こんなインタフェース IFooAccess を書きます。
public interface IFooAccess {
string name { get; } (1)
int SomeMethod(string a); (2)
void FireSomeEvent(EventArgs args); (3)
}
そして、実行時にはこのようにするだけです。
Foo foo = (アクセスしたいインスタンス)
PeepHoleBuilder bld = new PeepHoleBuilder(typeof(Foo), typeof(IFooAccess));
IFooAccess a = (IFooAccess)bld.CreateAccessor(foo);
string name = a.name; //fooの_nameが取れます!
a.SomeMethod("zzz"); //fooのSomeMethodが呼べます!
a.FireSoomeEvent(new EventArgs()); //fooのSomeEventイベントが発生します!
すると、IFooAccessインタフェースに書いたプロパティやメソッドを呼び出すことで、fooの対応するフィールドにアクセスしたりイベントを発行したりできるのです! あなたが書くのはIFooAccessインタフェースだけで、IFooAccessを実装したクラスは実行時にObjectPeepHoleが生成してくれるのです。
private/protectedフィールドのアクセスをするには、インタフェースにプロパティを宣言します。名前は先頭のアンダースコアを除いて一致し、型は同じでなければいけません。フィールドのセットがしたい場合はインタフェースのプロパティに { get; set; }とすればOKです。また、対象のフィールドがstaticでも大丈夫です。
private/protectedメソッドの呼び出しをするには、名前・引数の構成・戻り値の型がすべて一致するメソッドをインタフェースに書きます。対象のメソッドがstaticでも大丈夫です。
イベントの発行をするには、次の名前でメソッドをインタフェースに宣言します。
さらに、対象のクラスのフィールドのイベントを発行することもできます。これはダイアログボックスのテスト等で便利で、
class SomeForm : Form {
private Button _button1;
}
とあるときに、
interface SomeFormAccess {
void FireButton1_Click(EventArgs args);
}
と書くと_button1のClickイベントが発行できます。
このスタイルのルールは、
そもそも、イベントの発行はそれを宣言したクラスからしか行えない(派生クラスでもダメ)というのが厳しい制約です。これは知っている人も多いと思います。
イベントの発行はEventInfo.GetRaiseMethod を使って呼び出せばよいと思われた方は鋭いです。ところが、実際これを呼び出すとnullが返るのでだめなのです!
そこで、ildasmを使ってSystem.Windows.Forms.dllを読み込み、どのような手順でイベントを発行しているのかを調べてみると、次の2種類があることがわかりました。WinFormsには無数のクラスとイベントがありますが意外と少ないですね。
(1) delegateがフィールドとして存在する場合
イベントに対応した名前で、Delegate型のフィールドがあるパターンです。例えば, TreeViewにAfterLabelEditイベントハンドラを追加するILコードを読みだしたらこんな感じです。
.method public hidebysig specialname instance void
add_AfterLabelEdit(class System.Windows.Forms.NodeLabelEditEventHandler 'value') cil managed
{
IL_0000: ldarg.0
IL_0001: dup
IL_0002: ldfld class System.Windows.Forms.NodeLabelEditEventHandler System.Windows.Forms.TreeView::onAfterLabelEdit
IL_0007: ldarg.1
IL_0008: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_000d: castclass System.Windows.Forms.NodeLabelEditEventHandler
IL_0012: stfld class System.Windows.Forms.NodeLabelEditEventHandler System.Windows.Forms.TreeView::onAfterLabelEdit
IL_0017: ret
}
(2) イベントに対応したstaticなキーを使う場合
イベントの種類ごとにstaticなオブジェクトがあり、これをキーとしてEventHandlerListに問い合わせるパターンです。例えば, ControlにDoubleClickイベントハンドラを追加するILコードを読みだしたらこんな感じです。
.method public hidebysig specialname instance void
add_DoubleClick(class [mscorlib]System.EventHandler 'value') cil managed
{
IL_0000: ldarg.0
IL_0001: call instance class [System]System.ComponentModel.EventHandlerList [System]System.ComponentModel.Component::get_Events()
IL_0006: ldsfld object System.Windows.Forms.Control::EventDoubleClick
IL_000b: ldarg.1
IL_000c: callvirt instance void [System]System.ComponentModel.EventHandlerList::AddHandler(object, class [mscorlib]System.Delegate)
IL_0011: ret
}
どちらのパターンを使うかはクラスごとに異なるようでしたが、1つのクラス内でイベントの種類ごとに違うということはないみたいです。また、ユーザ自身が書いたクラスのイベントは(2)のパターンになるようです。各クラスがどちらのパターンに従うかがわかったら、その構造に沿う形でイベントハンドラを呼び出すILコードを生成しました。
また、イベントに関連づけられたメソッド(Clickイベントに対するOnClickなど)を呼ぶことでも関連づけたデリゲートが呼ばれることは呼ばれますが、OnClickではデリゲートを呼ぶ以外の動作もしているため副作用が起きる危険があります。状況に応じて使い分ければよいでしょう。
ObjectPeepHoleの主要なアイデアは、ユーザが書くのはインタフェースだけで、それを実装したクラスは実行時に動的に作るというものです。この動的なクラス内の各メソッドはILを直接生成するので、割と柔軟に動作できるはずなのですが、ldfldやcallといったインストラクションはprivate/protectedな対象には使用できないみたいです。実行するとFieldAccessException等の例外が発生します。アセンブリのセキュリティ関係の設定次第で何とかなるのかもしれませんがそれは不明なままでした。もし情報があれば教えてくれると助かります。
そこで仕方なく、FieldInfo.GetValueやMethodInfo.Invokeを呼び出すようなILを生成してシノいでいます。特にInvokeは引数をobject[]に詰めなおす必要があり、となるとboxingや参照型のことを考慮する必要があって面倒きわまりないんですが仕方ないですね。