はじめに
.NET Frameworkを利用したプロジェクトの単体テストでは, PrivateObject
クラスを利用して構築していました.
ただ, .NETからPrivateObject
クラスが存在しないようなので, 過去に作成した単体テストプロジェクトで使用していたPrivateObject
クラスを自作することにしました.
PrivateObjectクラス
今回自作するPrivateObject
クラスは, 過去に作成したテストコードがそのまま利用できることを前提としました. そのため, 以下のメソッドを作成します.
- Invoke
- GetField
- SetField
- GetProperty
- SetProperty
上記メソッドはリフレクションを利用すれば実現できます.
そのため, コンストラクタでテスト対象のオブジェクトと型を取得します.
using System.Reflection;
namespace TestTools
{
public class PrivateObject
{
private readonly static BindingFlags InstanceFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private readonly object Instance;
private readonly Type InstanceType;
public PrivateObject(object obj)
{
ArgumentNullException.ThrowIfNull(obj);
Instance = obj;
InstanceType = Instance.GetType();
}
}
}
あとは, それぞれの目的のメソッドを実装すれば完成です.
public object Invoke(string name, params object[] args)
{
try
{
var result = InstanceType.InvokeMember(name,
InstanceFlags | BindingFlags.InvokeMethod, null, Instance, args);
return result is null
? throw new NullReferenceException(name)
: result;
}
catch (Exception ex)
{
throw ex.InnerException ?? ex;
}
}
public object Invoke(string name)
=> Invoke(name, Array.Empty<object>());
public object GetField(string name)
{
try
{
var result = InstanceType.GetField(name, InstanceFlags)?.GetValue(Instance);
return result is null
? throw new NullReferenceException(name)
: result;
}
catch (Exception ex)
{
throw ex.InnerException ?? ex;
}
}
public void SetField(string name, object value)
{
try
{
InstanceType.GetField(name, InstanceFlags)?.SetValue(Instance, value);
}
catch (Exception ex)
{
throw ex.InnerException ?? ex;
}
}
public object GetProperty(string name)
{
try
{
var result = InstanceType.GetProperty(name, InstanceFlags)?.GetValue(Instance);
return result is null
? throw new NullReferenceException(name)
: result;
}
catch (Exception ex)
{
throw ex.InnerException ?? ex;
}
}
public void SetProperty(string name, object value)
{
try
{
InstanceType.GetProperty(name, InstanceFlags)?.SetValue(Instance, value);
}
catch (Exception ex)
{
throw ex.InnerException ?? ex;
}
}
ひとまずこれで当初の目的 (「過去に作成したテストコードがそのまま利用できる」) のPrivateObject
クラスは作成できました.
せっかくなので, 追加でバッキングフィールドの値をセットするメソッドも追加します.
バッキングフィールドの値変更のテストコードを書く際に, いつも書き方を忘れて調べていたので...。
public void SetBackingField(string name, object value)
{
try
{
InstanceType.GetField($"<{name}>k__BackingField", InstanceFlags)?.SetValue(Instance, value);
}
catch (Exception ex)
{
throw ex.InnerException ?? ex;
}
}
最後に
.NET用のPrivateObject
を自作したことで, これまで.NET Frameworkで作成していたテストコードを再利用できます.