LoginSignup
4
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-05

はじめに

 .NET Coreも3.* になったし、そろそろ.NET Frameworkで作ったプロジェクトも置き換えようと始めてみたのですが、単体テストにPrivateObjectがないことに気がつきました。
 あれ? それじゃあプライベートメソッドのテストが出来ないじゃないですか? どういうことですか!?

.NET Core と .NET Standard での単体テストのベスト プラクティス
https://docs.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-best-practices

パブリック メソッドの単体テストを行うことでプライベート メソッドを検証する
ほとんどの場合、プライベート メソッドをテストする必要はありません。 プライベート メソッドは実装の詳細です。 プライベート メソッドは独立して存在することはない、と考えることができます。

 ご、ごめんなさい!!
 確かにそうですね。プライベートメソッドのテストを書きながら「面倒だなぁ。でもやっておいた方がなんか安心するしなぁ」という感じで作ってました。あと単純にテストのパターン数が減らせるので。1
 とはいえ、既に作っちゃったわけで、これからは心を入れ替えて Publicだけでやっていくんで、既存部分だけは何とかならないかなと思うわけです。
 一時的な措置ということでリフレクションを使って代替案としたいと思ったのが始まりです。

置き換え案(初期案)

 初期に考えたものです。課題点があります。
 説明はコメントとして書いておきました。

PrivateObjectの代替クラス(初期案)
using System;
using System.Linq;
using System.Reflection;

    /// <summary>
    /// .Net Coreで消えた PrivateObjectの代替クラス。
    /// プライベートメソッドのテストに使用する
    /// </summary>
    public class PrivateObject
    {
        private readonly object _instance;
        private readonly Type _testTargetType;

        /// <summary>
        /// コンストラクタの引数が存在しないパターン
        /// </summary>
        /// <param name="testTargetClass">テスト対象のクラス</param>
        public PrivateObject(object testTargetClass)
        {
            _testTargetType = testTargetClass.GetType();   // テスト対象のタイプを取得            
            var c = _testTargetType.GetConstructor(Type.EmptyTypes);   // コンストラクタ情報を取得して            
            _instance = c.Invoke(null);  // インスタンス化(コンストラクタの引数なし)            
            SetAllPropertiesToInstance(testTargetClass);  // プロパティ類を全部コピー
        }

        /// <summary>
        /// コンストラクタの引数が存在するパターンはこちら。
        /// 引数は可変長になっているからいくつ引数があっても大丈夫。
        /// </summary>
        /// <param name="testTargetClass"></param>
        /// <param name="constArg"></param>
        public PrivateObject(object testTargetClass, params object[] constArg)
        {            
            _testTargetType = testTargetClass.GetType();  // テスト対象のタイプを取得

            // コンストラクタ情の引数の型情報を取得する
            var ctors = _testTargetType.GetConstructors();
            var ctor = ctors[0];
            var t = ctor.GetParameters().Select(a => a.ParameterType).ToArray();

            var c = _testTargetType.GetConstructor(t);   // コンストラクタ情報を引数を含めて取得して            
            _instance = c.Invoke(constArg);  // 引数込みでインスタンス化            
            SetAllPropertiesToInstance(testTargetClass);  // プロパティ類を全部コピー
        }

        /// <summary>
        /// インスタンスに全てのプロパティ情報をセットする
        /// </summary>
        /// <param name="testTargetClass"></param>
        private void SetAllPropertiesToInstance(object testTargetClass)
        {
            foreach (var t in _testTargetType.GetProperties())
            {
                SetPropertyToInstance(testTargetClass, t.Name);
            }
        }

        /// <summary>
        /// インスタンスにプロパティ情報をセットする
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="PropertyName"></param>
        private void SetPropertyToInstance(object obj, string PropertyName)
        {            
            var prop = _testTargetType.GetProperty(PropertyName); // プロパティ名からプロパティ情報を取得する          
            var value = prop.GetValue(obj);   // プロパティ値を抜き出して、            
            prop.SetValue(_instance, value);  // セットする
        }

        /// <summary>
        /// プライベートメソッドを実行する
        /// </summary>
        /// <param name="methodName"></param>
        /// <param name="arg"></param>
        /// <returns></returns>
        public object Invoke(string methodName, params object[] arg)
        {
            // メソッド名からメソッド情報を取得する。
            var method = _testTargetType.GetMethod(methodName, BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance);
            try
            {                
                return method.Invoke(_instance, arg);  // メソッドを実行する
            }
            catch (Exception e)
            {
                // エラーが発生したら「ここ」のエラー情報じゃなくて発生元のエラーを投げる
                throw e.InnerException;
            }
        }
    }
使用方法(テスト対象のコンストラクタに引数がない場合)
// Act
var privObj = new PrivateObject(testTargetClass);
var result = privObj.Invoke("PrivateMethod", arg1, arg2);
使用方法(テスト対象のコンストラクタに引数ががある場合)
// Act
var privObj = new PrivateObject(testTargetClass, constArg1, constArg2);
var result = privObj.Invoke("PrivateMethod", arg1, arg2);

課題点

 お気づきのように、テスト対象のコンストラクタに引数がない場合は良いのですが、ある場合はPrivateObjectのインスタンス化時に、その引数も渡してあげる必要がある状態となっています。
 それを無くすためにはテストオブジェクト対象の内容をそのままコピーすることが出来ればいいわけで、原理的には出来るはずだと思うのですが(.NET Frameworkなら出来ていたわけで)、生憎と今の私の拙いリフレクション知識ではこれが精一杯です。これだけでも大分軽減できたのでまずは十分です。

置き換え案2(課題点解決)

 色々調べたところ、InvokeMemberを使えば解決できるということが分かりました。

PrivateObjectの代替クラス(上記の課題点を解決)
using System;
using System.Reflection;

    /// <summary>
    /// .Net Coreで消えた PrivateObjectの代替クラス。
    /// プライベートメソッドのテストに使用する
    /// </summary>
    public class PrivateObject
    {
        private readonly object _obj;
        public PrivateObject(object obj)
        {
            _obj = obj;
        }

        public object Invoke(string methodName, params object[] args)
        {
            var type = _obj.GetType();
            var bindingFlags = BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance;
            try
            {
                return type.InvokeMember(methodName, bindingFlags, null, _obj, args);
            }
            catch (Exception e)
            {
                throw e.InnerException;
            }
        }
    }

 シンプルで良し!
 もちろん元々のを完全に再現しているわけではないので、色々と足りないところはありますが、まぁ十分かなと。
 こういうメタプログラミングも学んでいかないといけないですね。

参考


  1. publicで3分岐、privateで4分岐があったとしたら、privateのテストもやれば3+4の足し算で済むけど、publicのみだと、3×4の乗算になるので。 

4
4
1

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
4
4