.NET 単体テストのPrivateObjectを自作する

# はじめに
.NET Frameworkを利用したプロジェクトの単体テストでは, ```PrivateObject```クラスを利用して構築していました.
ただ, .NETから```PrivateObject```クラスが存在しないようなので, 過去に作成した単体テストプロジェクトで使用していた```PrivateObject```クラスを自作することにしました. 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196571/dfd4af84-e2f5-256a-9f2e-b3d578bdea20.png)


# PrivateObjectクラス
今回自作する```PrivateObject```クラスは, 過去に作成したテストコードがそのまま利用できることを前提としました. そのため, 以下のメソッドを作成します.
- Invoke
- GetField
- SetField
- GetProperty
- SetProperty

上記メソッドはリフレクションを利用すれば実現できます.
そのため, コンストラクタでテスト対象のオブジェクトと型を取得します.
```cs
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();
        }
    }
}
```

<br> 
あとは, それぞれの目的のメソッドを実装すれば完成です.

```cs
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```クラスは作成できました.

せっかくなので, 追加でバッキングフィールドの値をセットするメソッドも追加します.
バッキングフィールドの値変更のテストコードを書く際に, いつも書き方を忘れて調べていたので...。

```cs
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で作成していたテストコードを再利用できます.


# 参考
[.NetCore ではPrivateObjectが無いのでその代替案](https://qiita.com/saeki4n/items/1cafac3f4451e517ae06)