Friendlyは、統合テストを作成するためのライブラリです。
他のプロセスを操作する機能があります。
Windowsアプリケーション(WinForms, WPF, and Win32)用に設計されています。
Friendlyという名前は、C++ の friend クラスに由来します。
対象のプロセスとFriendになることでどのような操作でも可能にします。
.NET 9 から BinaryFormatter が廃止されます。Friendlyではプロセス間通信でのデータのシリアライズにこれを使っています。シリアライズをカスタマイズできるようにしましたので.NET環境ではこちらをご利用ください。
SeleniumのPageObjectパターンと同じようにFriendlyを使った自動テストでも推奨される設計方針があります。こちらも参照してください。
Friendlyは、.NetCore WinFormsおよびWPFアプリも操作できます。テストコードも.NetCoreで書くことができます。Friendlyは.NetFrameworkでてきているので参照時にワーニングがでます。問題なく使えますのでワーニングを抑制してください。
これは、SeleniumのJavaScript実行のようなものです。
public internal protected private に関わらず、すべての Method、Property、Field を呼び出すことができます。
.Netのアセンブリを Dll Injection することができます。 インジェクションした dll に含まれるAPIは同様に呼び出し可能です。
Install from NuGet
WPF
PM> Install-Package RM.Friendly.WPFStandardControls
WinForms
PM> Install-Package Ong.Friendly.FormsStandardControls
Win32
PM> Install-Package Codeer.Friendly.Windows.NativeStandardControls
https://www.nuget.org/packages/Codeer.Friendly/
http://blogs.msdn.com/b/mvpawardprogram/archive/2014/11/04/mvp-showcase-winners.aspx
これは操作対象のWpfアプリです。何の仕掛けもない普通のWpfアプリです。
<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>
<TextBox x:Name="_textBox" Text="{Binding Path=TextData}"/>
</Grid>
</Window>using System.ComponentModel;
using System.Windows;
namespace Target
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new VM();
}
string MyFunc(int value)
{
return value.ToString();
}
}
class VM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (_, __) => { };
string _textData;
public string TextData
{
get { return _textData; }
set
{
_textData = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TextData)));
}
}
}
}こちらがテストコードです。先ほどのWpfアプリを操作します。
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics;
using System.IO;
using System.Windows;
namespace Sample
{
[TestClass]
public class Test
{
WindowsAppFriend _app;
[TestInitialize]
public void TestInitialize()
{
//アタッチ
var path = Path.GetFullPath("../../../Target/bin/Debug/Target.exe");
_app = new WindowsAppFriend(Process.Start(path));
}
[TestCleanup]
public void TestCleanup()
{
Process process = Process.GetProcessById(_app.ProcessId);
_app.Dispose();
process.CloseMainWindow();
}
[TestMethod]
public void Manipulate()
{
// staticなメソッドを呼び出し
dynamic window = _app.Type<Application>().Current.MainWindow;
// instanceのメソッドを呼び出し
string value = window.MyFunc(5);
Assert.AreEqual("5", value);
// instanceのプロパティに値を設定
window.DataContext.TextData = "abc";
// instanceのフィールドを取得
string text = window._textBox.Text;
Assert.AreEqual("abc", text);
}
}
}FriendlyのAPI呼び出し機能は非常に強力です。.Netの知識があれば、ほとんどの操作が可能です。
ただ、これだけでテストを書くことは難しいので、より便利なライブラリを用意しています。

基本機能です。別プロセスのAPI呼び出しとDllインジェクションの機能を提供します。
他のFriendlyのライブラリ群はこの機能の上に構築されています。
主にWindowを検索する機能を提供します。他にはもWindowハンドルを持つWindowに対してWin32レベルの基本操作を提供します。
低レベルのキーマウスのエミュレーションです。
Friendlyの機能を使ってタイミングが調整されています。
Button, ListView, TreeView などの基本的なコントロールの操作を提供します。
Friendly.Windows.NativeStandardControls(Win32)
Friendly.FormsStandardControls(WinForms)
Friendly.WPFStandardControls(WPF)
Friendly.XamControls(WPF)
Friendly.FarPoint(WinForms)
Friendly.C1.Win(WinForms)
例えばWPFアプリを操作するときにこれらのパッケージを使います。
Friendly.Windows.Grasp
Friendly.WPFStandardControls
Friendly.NativeStandardContorls
using System.Diagnostics;
using System.IO;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RM.Friendly.WPFStandardControls;
namespace Scenario
{
[TestClass]
public class Test
{
WindowsAppFriend _app;
[TestInitialize]
public void TestInitialize()
{
var path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), "../../../DemoApp/bin/Debug/DemoApp.exe"));
var info = new ProcessStartInfo(path) { WorkingDirectory = Path.GetDirectoryName(path) };
_app = new WindowsAppFriend(Process.Start(info));
}
[TestCleanup]
public void TestCleanup() => Process.GetProcessById(_app.ProcessId).Kill();
[TestMethod]
public void Sample()
{
// .netのタイプからウィンドウを取得
var window = _app.WaitForIdentifyFromTypeFullName("DemoApp.Views.MainWindow");
// フィールドからユーザーコントロールを取得(Friendlyの基本機能)
AppVar userControl = window.Dynamic()._userControl;
// バインディングからDataGridを取得
WPFDataGrid dataGrid = userControl.LogicalTree().ByBinding("SelectedItem.Value").Single().Dynamic();
// edit.
dataGrid.EmulateChangeCellText(1, 2, "abc");
// ※これはサンプルなので混ぜて書いていますが、通常はSeleniumのPageObjectのように要素の特定部分は分離して記述します。
}
}
}ここでは、Friendlyの基本的な機能について説明します。
別のプロセスのAPI呼び出しとDLLインジェクションです。
操作対象のプロセスとテストプロセスは、同じプロセッサアーキテクチャを使用する必要があります。
VSTestを使用している場合は、次のようにVisual Studioメニューを使用して設定できます。

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;WindowsAppFriendを使用してアタッチします。
Processを渡すコンストラクタを使うと操作はメインウィンドウのスレッドで実行されます。
public WindowsAppFriend(Process process);ウィンドウハンドルを渡すコンストラクタを使うと、そのウィンドウが動作するスレッドで実行されます。
public WindowsAppFriend(IntPtr windowHandle);dynamic sampleForm1 = _app.Type<Application>().Current.MainWindow;
dynamic sampleForm2 = _app.Type(typeof(Application)).Current.MainWindow;
dynamic sampleForm4 = _app.Type("System.Windows.Forms.Application").Current.MainWindow;//method
string value = window.MyFunc(5);
//property
window.DataContext.TextData = "abc";
//field.
string text = window._textBox.Text;インターフェイスの詳細に関してはこちらを参照してください。
dynamic listBox1 = _app.Type<ListBox>()();
dynamic listBox2 = _app.Type(typeof(ListBox))();
dynamic listBox4 = _app.Type("System.Windows.Controls.ListBox")();
dynamic list = _app.Type<List<int>>()(new int[]{1, 2, 3, 4, 5});シリアライズ可能なオブジェクト/AppVar/DynamicAppVar/IAppVarOwnerを指定することができます。
シリアライズ可能なオブジェクトを使用すると、それらはシリアライズされ、コピーがターゲットプロセスに送信されます。
AppVar/DynamicAppVar/IAppVarOwnerの詳細に関してはこちらを参照してください。
// シリアライズ可能なオブジェクト
window.MyFunc(5);
window.DataContext.TextData = "abc";
// 対象プロセス内にテキストボックスのインスタンスを生成(DynamicAppVar)
dynamic textBox = _app.Type<TextBox>()();
// DynamicAppVar
window.Content.Children.Add(textBox);// DyamicAppVar 対象プロセス内のオブジェクトを操作します。
dynamic reference = window._textBox.Text;
// キャストするとシリアライズされてこちらのプロセスにコピーされてきます。
string text = reference;// OK DynamicObjectのTryConvertにより変換されます。
string cast = (string)reference;
// OK DynamicObjectのTryConvertにより変換されます。
string substitution = reference;
// falseになります。
bool isString = reference is string;
// nullになります。
string textAs = reference as string;
// 例外が発生します。C#の仕様です。operatorによる変換ではないのでそうなります。
string.IsNullOrEmpty(reference);
// OK
string.IsNullOrEmpty((string)reference);IEnumerable
foreach (var w in _app.Type<Application>().Current.Windows)
{
}AppVar
dynamic window = _app.Type<Application>().Current.MainWindow;
AppVar appVar = window;
appVar["Title"]("abc");AppVar は古いスタイルのインターフェイスです。
4.0より古いバージョンの.Netでテストを書く場合には使う必要があります。
Friendlyのライブラリの引数や戻り値にも登場します。詳細はこちらを参照してください。
DynamicAppVarは、AppVarを1つの引数として取るコンストラクターを持つクラスに暗黙的に変換できます。
var window = app.Type<Application>();
//pulbic WPFDataGrid(AppVar src)
WPFDataGrid dataGrid = new WPFDataGrid(window._dataGrid);
//can convert!
WPFDataGrid dataGrid = window._dataGrid;Friendlyの操作は通常はプロセス間で同期されます。
Asyncを使うと非同期にできます。
モーダルダイアログの操作時に使います。
// 非同期は引数のどこにでも指定できます。
var async = new Async();
window.MyFunc(async, 5);
// 完了したかを確認できます。
if (async.IsCompleted)
{
//・・・
}
// 処理が完了するまで待つことができます。
async.WaitForCompletion();戻り値は処理が完了したときに取得することができます。
// 非同期で実行
var async = new Async();
var text = window.MyFunc(async, 5);
// 処理が終了したら値を取得することができます。
async.WaitForCompletion();
string textValue = (string)text;Dictionary<int, string> dic = new Dictionary<int, string>();
dic.Add(1, "1");
// オブジェクトのコピーを対象プロセス内に作成します。
dynamic dicInTarget = _app.Copy(dic);
// 特殊な使い方です。対象プロセス内にnullの変数を作成します
dynamic value = _app.Null();
dicInTarget.TryGetValue(1, value);
Assert.AreEqual("1", (string)value);[TestMethod]
public void DllInjection()
{
dynamic window = _app.Type<Application>().Current.MainWindow;
dynamic textBox = window._textBox;
//指定のアセンブリを対象プロセスにロードさせます。
_app.LoadAssembly(GetType().Assembly);
//ロードさせたアセンブリに含まれる型を対象プロセス内でつかうことができます。
dynamic observer = _app.Type<Observer>()(textBox);
//Check change text.
textBox.Text = "abc";
Assert.IsTrue((bool)observer.TextChanged);
}
class Observer
{
internal bool TextChanged { get; set; }
internal Observer(TextBox textBox)
{
textBox.TextChanged += delegate { TextChanged = true; };
}
}Native dll methods.
[TestMethod]
public void DllInjectionPInvoke()
{
WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);
Process process = Process.GetProcessById(_app.ProcessId);
_app.Type(GetType()).MoveWindow(process.MainWindowHandle, 0, 0, 200, 200, true);
dynamic rectInTarget = _app.Type<RECT>()();
_app.Type(GetType()).GetWindowRect(process.MainWindowHandle, rectInTarget);
RECT rect = (RECT)rectInTarget;
Assert.AreEqual(0, rect.left);
Assert.AreEqual(0, rect.top);
Assert.AreEqual(200, rect.right);
Assert.AreEqual(200, rect.bottom);
}
[DllImport("User32.dll")]
static extern bool MoveWindow(IntPtr handle, int x, int y, int width, int height, bool redraw);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
[Serializable]
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}Friendlyは当初、.Net 2.0で動作するように設計されました。
そのため、以前はこのような呼び出しを行っていました。
//old style
AppVar mainWindow = app[typeof(Application), "Current"]()["MainWindow"]();
string title = (string)mainWindow["Title"]().Core;.Net4.0でダイナミックを使うことができるようになったのでこのように自然に書くことができるようになりました。
//new style
dynamic mainWindow = app.Type<Application>().Current.MainWindow;
string title = mainWindow.Title;通常、.Net 4.0より前のバージョンを使用する必要はないので、新しいスタイルでコードを記述できます。
ただ、dynamicは関数の引数や戻り値に最適ではないため、ライブラリのインターフェイスにAppVarは引き続き使用されます。
Friendlyは最終的に、リフレクションを使用してターゲットプロセスでAPIを実行します。
そのため操作は文字列で指定されます。
上記の例ではプロパティですが、例えば関数呼び出しは次のように書きます。
関数名は[]で文字列として指定して、引数は続く()で渡します。
mainWindow["MyFunc"](100);ただし、これは直感的ではありません。
.Net4.0はダイナミックを使用できるため、DynamicAppTypeおよびDynamicAppVarが導入されました。
その結果、上記の new style のように直感的に書けるようになりました。
using Codeer.Friendly.Dynamic;Codeer.Friendly.Dynamic namespace を using することによって拡張メソッドが使えるようになります。
AppVar mainWindow1 = app[typeof(Application), "Current"]()["MainWindow"]();
//1. Dynamic() AppVar -> dynamic(DynamicAppVar)
dynamic mainWindow2 = mainWindow1.Dynamic();
//2. Type()
dynamic applicationType = app.Type<Application>();
dynamic mainWindow3 = applicationType.Current.MainWindow;DynamicAppTypeとDynamicAppVarはどちらも、API呼び出しの戻り値としてdynamic(DynamicAppVar)を返します。
そのため同じプロセスの通常の.Netオブジェクトであるかのように、APIを呼び出すことができます。
DynamicAppVarとAppVarは相互に変換できます。
AppVarを引数として使用するFriendlyのライブラリがあり、ここにはDynamicAppVarを使うことができます。
var window = app.Type<Application>();
//pulbic WPFDataGrid(AppVar src)
WPFDataGrid dataGrid = new WPFDataGrid(window._dataGrid);dynamic mainWindow = app.Type<Application>().Current.MainWindow;
//DynamicAppVar
//この時点では対象プロセスの中にあります。
dynamic title = mainWindow.Title;
//stringに代入した時点で対象プロセスからコピーされてきます。
string titleText = title;dynamic windowSrc = app.Type<Application>().Current.MainWindow;
//例外発生!
Window windowDst = windowSrc;
//[Codeer.Friendly.FriendlyOperationException]
//アプリケーションとの通信に失敗しました。
//対象アプリケーションが通信不能な状態になったか、
//シリアライズ不可能な型のデータを転送しようとした可能性があります。これはWindowクラスがシリアライズできないために発生します。
このようなオブジェクトはDynamicAppVarのままで使ってください。

IAppVarOwnerは内部にAppVarを持つクラスであることを明示するためのインターフェイスです。 このインターフェイスを実装しているクラスは以下のメリットがあります。
- AppVar同様に引数に渡すことができる
- Dynamic拡張メソッドが使えるようになる
[TestMethod]
public void Test()
{
//WindowControlはIAppVarOwnerを実装しています。
WindowControl window = _app.WaitForIdentifyFromTypeFullName("DemoApp.Views.MainWindow");
//Dynamic拡張が使えます。
//WPFTextBoxもIAppVarOwnerを実装しています。
WPFTextBox textBox = new WPFDataGrid(window.Dynamic()._textBox);
//FriendlyのインターフェイスにAppVar同様に渡すことができます。
_app.LoadAssembly(GetType().Assembly);
dynamic observer = _app.Type<Observer>()(textBox);
//Check change text.
textBox.EmulateChangeText("abc");
Assert.IsTrue((bool)observer.TextChanged);
}
class Observer
{
internal bool TextChanged { get; set; }
internal Observer(TextBox textBox)
{
textBox.TextChanged += delegate { TextChanged = true; };
}
}
