LoginSignup
0
0

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

Posted at

はじめに

.NET Frameworkを利用したプロジェクトの単体テストでは, PrivateObjectクラスを利用して構築していました.
ただ, .NETからPrivateObjectクラスが存在しないようなので, 過去に作成した単体テストプロジェクトで使用していたPrivateObjectクラスを自作することにしました.
image.png

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

参考

.NetCore ではPrivateObjectが無いのでその代替案

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0