• C# 非public的方法和属性的单元测试


    有时候我们写好的类库中,某些类的属性和方法不应该暴露出来,那么如何对这些非public的方法和属性进行单元测试?

    MS为我们提供了PrivateObject类,可以解决这个问题,可以去MSDN的说明文档中查看这个类的介绍。本文也试举一例,说明其用法。

    首先给出被测试类,为了比较全面,我们在其中包含了public属性、protected属性、private属性等等,如下

      /// <summary>
      /// Implementation of a class with non-public members.
      /// </summary>
      public class Testee
      {
        #region Public Properties
    
        /// <summary>
        /// Gets / sets the value of a public property.
        /// </summary>
        public string PublicProperty { get; set; }
    
        #endregion Public Properties
    
    
        #region Protected Properties
    
        /// <summary>
        /// Gets / sets the value of a protected property.
        /// </summary>
        protected string ProtectedProperty { get; set; }
    
        #endregion Protected Properties
    
    
        #region Private Properties
    
        /// <summary>
        /// Gets / sets the value of a private property.
        /// </summary>
        private string PrivateProperty { get; set; }
    
        #endregion Private Properties
    
    
        #region Private Static Properties
    
        /// <summary>
        /// Gets / sets the value of a private static property.
        /// </summary>
        private static string PrivateStaticProperty { get; set; }
    
        #endregion Private Static Properties
    
    
        #region Protected Methods
    
        /// <summary>
        /// A simple protected method with a parameter.
        /// </summary>
        /// <param name="parameter">The parameter</param>
        /// <returns>Another string.</returns>
        protected string ProtectedMethod(string parameter)
        {
          Console.WriteLine("Parameter: >{0}<", parameter);
    
          return (parameter + " processed");
        }
    
        #endregion Protected Methods
      }

    然后,我们需要一个类,命名为PrivateAccessor,这个类提供访问非public属性和非public方法的方式。PrivateAccessor类中包含一个PrivateObject类对象,其实本质就是通过这个PrivateObject对象实现访问非public属性以及非public方法,如下所示代码

    class PrivateAccessor
    {
        // other members
        private PrivateObject PrivateObject { get; set; }
    }    

    因为我们要访问被测试类Testee的非public属性和非public方法,故需要传入此类的实例。另外,由于被测试类中含有静态的非public属性,在包装类中则需要一个对应的静态属性信息集合,用于保存被测试类Testee中所有的静态非public属性。

    现在,代码变为如下

    class PrivateAccessor<T>
    {
        // other members
        private PrivateObject PrivateObject { get; set; }  

        /// <summary>
        /// Gets / sets the declared proporties for later use, in case static properties should be accessed.
        /// </summary>
        private static IEnumerable<PropertyInfo> DeclaredProperties { get; set; }

        /// <summary>
        /// Static initialization.
        /// </summary>
        static PrivateAccessor()
        {
          // Get the declared properties for later use, in case static properties should be accessed.
          PrivateAccessor<T>.DeclaredProperties = typeof(T).GetProperties(BindingFlags.NonPublic | BindingFlags.Static);
        }
    
        internal PrivateAccessor(T instance)
        {
             PrivateObject = new PrivateObject(instance);
        }
    }

    到此,我们知道,PrivateAccessor类中,还缺少访问Testee类中非public方法和属性的辅助方法,不过先不急,先看看这个PrivateAccessor类,这是一个泛型类,泛型参数为T,并非我们这里所要测试的类Testee。

    我们可以考虑再新增一个类,继承这个PrivateAccessor<T>泛型类,并提供泛型参数Testee,另外再给出访问被测试类Testee中的各个非public方法和属性的入口,在这些入口中调用PrivateAccessor中的那些辅助方法,虽然这些方法还未实现。

    代码如下

      class MembersAccessWrapper : PrivateAccessor<Testee>
      {
        #region Interal Constructors
        internal MembersAccessWrapper()
          : base(new Testee())
        {
        }
        #endregion Interal Constructors
    
    
        #region Internal Properties
    
        /// <summary>
        /// Gives get / set access to the property "ProtecedProperty".
        /// </summary>
        internal string ProtectedProperty
        {
          [MethodImpl(MethodImplOptions.NoInlining)]
          get { return (GetPropertyValue<string>()); }
          [MethodImpl(MethodImplOptions.NoInlining)]
          set { SetPropertyValue(value); }
        }
    
        /// <summary>
        /// Gives get / set access to the property "PrivateProperty".
        /// </summary>
        internal string PrivateProperty
        {
          [MethodImpl(MethodImplOptions.NoInlining)]
          get { return (GetPropertyValue<string>()); }
          [MethodImpl(MethodImplOptions.NoInlining)]
          set { SetPropertyValue(value); }
        }
    
        #endregion Internal Properties
    
    
        #region Internal Static Properties
    
        /// <summary>
        /// Gives get / set access to the static property "PrivateStaticProperty".
        /// </summary>
        internal static string PrivateStaticProperty
        {
          [MethodImpl(MethodImplOptions.NoInlining)]
          get { return (PrivateAccessor<Testee>.GetStaticPropertyValue<string>()); }
          [MethodImpl(MethodImplOptions.NoInlining)]
          set { PrivateAccessor<Testee>.SetStaticPropertyValue(value); }
        }
    
        #endregion Internal Static Properties
    
    
        #region Internal Methods
        /// <summary>
        ///  Calls the protected method ProtectedMethod
        /// </summary>
        /// <param name="parameter">The parameter</param>
        /// <returns>The return value of the called method.</returns>
        [MethodImpl(MethodImplOptions.NoInlining)]
        internal string ProtectedMethod(string parameter)
        {
          // Use the base class to invoke the correct method.
          return (Invoke<string>(parameter));
        }
        #endregion Internal Methods
      }

    现在,剩下的工作就是实现PrivateAccessor<T>类中的那些辅助方法,如下代码所示

    class PrivateAccessor<T>
    {
        // other members 

          /// <summary>
          /// Keeps the prefix of property getter method names.
          /// </summary>
          private const string GetterPrefix = "get_";

          /// <summary>
          /// Keeps the prefix of property setter method names.
          /// </summary>
          private const string SetterPrefix = "set_";

        #region Protected Methods
    
        /// <summary>
        /// Returns the value of a property. The name of the property is taken from the call stack (for .NET version < 4.5).
        /// </summary>
        /// <typeparam name="T">Type of the return value.</typeparam>
        /// <returns>The property value.</returns>
        [MethodImpl(MethodImplOptions.NoInlining)]
        protected TReturnValue GetPropertyValue<TReturnValue>()
        {
          // Need to remove the "get_" prefix from the caller's name
          string propertyName = new StackFrame(1, true).GetMethod().Name.Replace(PrivateAccessorBase<T>.GetterPrefix, String.Empty);
    
          return ((TReturnValue)PrivateObject.GetProperty(propertyName, null));
        }
    
        /// <summary>
        /// Sets the value of a property. The name of the property is taken from the call stack (for .NET version < 4.5).
        /// </summary>
        /// <param name="value">The value to be set.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        protected void SetPropertyValue
          (
          object value
          )
        {
          // Need to remove the "set_" prefix from the caller's name
          string propertyName = new StackFrame(1, true).GetMethod().Name.Replace(PrivateAccessorBase<T>.SetterPrefix, String.Empty);
    
          PrivateObject.SetProperty(propertyName, value, new object[] { });
        }
    
        /// <summary>
        /// Invokes a method with parameter an return value.
        /// </summary>
        /// <typeparam name="TReturnValue">The type of the return value.</typeparam>
        /// <param name="parameter">The parameter to be passed.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        protected TReturnValue Invoke<TReturnValue>
          (
          params object[] parameter
          )
        {
          // Take the caller's name as method name
          return ((TReturnValue)PrivateObject.Invoke(new StackFrame(1, true).GetMethod().Name, parameter));
        }
    
        #endregion Protected Methods
    
    
        #region Protected Static Methods
    
        /// <summary>
        /// Returns the value of a static property. The name of the property is taken from the call stack (for .NET version < 4.5).
        /// </summary>
        /// <typeparam name="T">Type of the return value.</typeparam>
        /// <returns>The property value.</returns>
        [MethodImpl(MethodImplOptions.NoInlining)]
        protected static TReturnValue GetStaticPropertyValue<TReturnValue>()
        {
          // Need to remove the "get_" prefix from the caller's name
          string propertyName = new StackFrame(1, true).GetMethod().Name.Replace(PrivateAccessorBase<T>.GetterPrefix, String.Empty);
    
          // Find the property having the matching name and get the value
          return ((TReturnValue)PrivateAccessorBase<T>.DeclaredProperties.Single(info => info.Name.Equals(propertyName)).GetValue(null, null));
        }
    
        /// <summary>
        /// Sets the value of a static property. The name of the property is taken from the call stack (for .NET version < 4.5).
        /// </summary>
        /// <param name="value">The value to be set.</param>
        /// <returns>The property value.</returns>
        [MethodImpl(MethodImplOptions.NoInlining)]
        protected static void SetStaticPropertyValue(object value)
        {
          // Need to remove the "set_" prefix from the caller's name
          string propertyName = new StackFrame(1, true).GetMethod().Name.Replace(PrivateAccessorBase<T>.SetterPrefix, String.Empty);
    
          // Find the property having the matching name and set the value
          PrivateAccessorBase<T>.DeclaredProperties.Single(info => info.Name.Equals(propertyName)).SetValue(null, value, null);
        }
    
        #endregion Protected Static Methods
    }

    如此就实现了访问非public的方法和属性。

    最后,给出调用代码

    [TestClass()]
    public class Tester
    {
        public TestContext TestContext {get; set;}
        //You can use the following additional attributes as you write your tests:
        //Use ClassInitialize to run code before running the first test in the class
        [ClassInitialize()]
        public static void MyClassInitialize(TestContext testContext)
        {
        }
        
        //Use ClassCleanup to run code after all tests in a class have run
        [ClassCleanup()]
        public static void MyClassCleanup()
        {
        }
        
        //Use TestInitialize to run code before running each test
        [TestInitialize()]
        public void MyTestInitialize()
        {
        }
        
        //Use TestCleanup to run code after each test has run
        [TestCleanup()]
        public void MyTestCleanup()
        {
        }
    [TestMethod]
    public void TestProtectedProperty() { var accessor = new MembersAccessWrapper(); accessor.ProtectedProperty = "Test String"; // Write it out for tracing ablilities Debug.WriteLine("ProtectedProperty: >{0}<{1}", accessor.ProtectedProperty, Environment.NewLine); // Test the value (means call the getter) Assert.AreEqual("Test String", accessor.ProtectedProperty, "Property has wrong value"); }
    }

    这段测试代码仅测试了protected属性,其他属性和方法的测试类似,不再重复写出。

    .Net Framework4.5之后,对属性的动态访问作了一个简单的改变。此外上面的讨论未涉及到异步任务的非public访问,我们也一并考虑。首先被测试类增加非public的异步方法

    class Testee
    {
        // other members
    
        /// <summary>
        /// Gets / sets the value of a private property.
        /// </summary>
        private static string PrivateStaticProperty { get; set; }
    
        #region Private Methods
    
        /// <summary>
        /// A private async method with two input parameters and a return value.
        /// </summary>
        /// <param name="millisecondsDelay">Delaytime in milliseconds..</param>
        /// <param name="outputText">Output text.</param>
        /// <returns>Delaytime in seconds.</returns>
        private async Task<double> PrivateMethodWithReturnValueAsync(int millisecondsDelay, string outputText)
        {
          // First wait.
          await Task.Delay(millisecondsDelay);
    
          // Write the output
          Console.WriteLine(">{0}< milliseconds delayed. Output: >{1}<", millisecondsDelay, outputText);
    
          // Need to cast to make sure the all values will be handled as double, not as int.
          return ((double)millisecondsDelay / (double)1000);
        }
    
        #endregion Private Methods
    
    
        #region Private Static Methods
    
        /// <summary>
        /// A private static async method with two input parameters and no return value.
        /// </summary>
        /// <param name="millisecondsDelay">Delaytime in milliseconds..</param>
        /// <param name="outputText">Output text.</param>
        private static async Task PrivateStaticVoidMethodAsync(int millisecondsDelay, string outputText)
        {
          // First wait.
          await Task.Delay(millisecondsDelay);
    
          // Do something that can be unit tested
          PrivateStaticProperty = outputText;
    
          // Write the output
          Console.WriteLine(">{0}< milliseconds delayed. Output: >{1}<", millisecondsDelay, outputText);
        }
    
        #endregion Private Static Methods
    }

    PrivateAccessor<T>类中增加属性信息的集合和方法信息的集合,用于保存被测试类Testee中的所有属性信息和方法信息,由于可能存在静态属性和静态方法,故这两个集合在类的静态构造函数中初始化。

    class PrivateAccessor<T>
    {
        // other members
    
        /// <summary>
        /// Gets / sets the declared proporties for later use, in case static properties should be accessed.
        /// </summary>
        private static IEnumerable<PropertyInfo> DeclaredProperties { get; set; }
    
        /// <summary>
        /// Gets / sets the declared methods for later use, in case static methods should be accessed.
        /// </summary>
        private static IEnumerable<MethodInfo> DeclaredMethods { get; set; }
    
        static PrivateAccessor()
        {
          TypeInfo typeInfo = typeof(T).GetTypeInfo();
    
          // Get the declared properties for later use, in case static properties should be accessed.
          PrivateAccessorBase<T>.DeclaredProperties = typeInfo.DeclaredProperties;
    
          // Get the declared methods for later use, in case static methods should be accessed.
          PrivateAccessorBase<T>.DeclaredMethods = typeInfo.DeclaredMethods;
        }
    }

    然后修改PrivateAccessor<T>类中那些辅助方法的实现

    class PrivateAccessor<T>
    {
        // other members
    
        #region Protected Methods
    
    
        /// <summary>
        /// Returns the value of a property. The name of the property is passed by using <see cref="CallerMemberName"/> (for .NET version >= 4.5).
        /// </summary>
        /// <typeparam name="TReturnValue">Type of the return value.</typeparam>
        /// <param name="callerName">Name of the calling method.</param>
        /// <returns>The property value.</returns>
        protected TReturnValue GetPropertyValue<TReturnValue>([CallerMemberName] string callerName = null)
        {
          // Take the caller's name as property name
          return ((TReturnValue)PrivateObject.GetProperty(callerName, null));
        }
    
        /// <summary>
        /// Sets the value of a property. The name of the property is passed by using <see cref="CallerMemberName"/> (for .NET version >= 4.5).
        /// </summary>
        /// <param name="value">The value to be set.</param>
        /// <param name="callerName">Name of the calling method.</param>
        protected void SetPropertyValue(object value, [CallerMemberName] string callerName = null)
        {
          // Take the caller's name as property name
          PrivateObject.SetProperty(callerName, value, new object[] { });
        }
    
        /// <summary>
        /// Invokes a method with parameter an return value.
        /// </summary>
        /// <typeparam name="TReturnValue">The type of the result.</typeparam>
        /// <param name="callerName">Name of the calling method.</param>
        /// <param name="parameter">The parameter to be passed.</param>
        protected TReturnValue Invoke<TReturnValue>([CallerMemberName] string callerName = null, params object[] parameter)
        {
          // Take the caller's name as method name
          return ((TReturnValue)PrivateObject.Invoke(callerName, parameter));
        }
    
        /// <summary>
        /// Invokes a async method with parameters and return value.
        /// </summary>
        /// <typeparam name="TReturnValue">The type of the result.</typeparam>
        /// <param name="callerName">Name of the calling method.</param>
        /// <param name="parameter">The parameter to be passed.</param>
        protected async Task<TReturnValue> InvokeAsync<TReturnValue>([CallerMemberName] string callerName = null, params object[] parameter)
        {
          // Take the caller's name as method name
          TReturnValue returnValue = await (Task<TReturnValue>)PrivateObject.Invoke(callerName, parameter);
    
          // return the result
          return (returnValue);
        }
    
        #endregion Protected Methods
    
    
        #region Protected Static Methods
    
        /// <summary>
        /// Returns the value of a static property. The name of the property is passed by using <see cref="CallerMemberName"/> (for .NET version >= 4.5).
        /// </summary>
        /// <typeparam name="TReturnValue">Type of the return value.</typeparam>
        /// <param name="callerName">Name of the calling method.</param>
        /// <returns>The property value.</returns>
        protected static TReturnValue GetStaticPropertyValue<TReturnValue>([CallerMemberName] string callerName = null)
        {
          // Find the property having the matching name and get the value
          return ((TReturnValue)PrivateAccessor<T>.DeclaredProperties.Single(info => info.Name.Equals(callerName)).GetValue(null));
        }
    
        /// <summary>
        /// Sets the value of a static property. The name of the property is passed by using <see cref="CallerMemberName"/> (for .NET version >= 4.5).
        /// </summary>
        /// <param name="value">The value to be set.</param>
        /// <param name="callerName">Name of the calling method.</param>
        protected static void SetStaticPropertyValue(object value, [CallerMemberName] string callerName = null)
        {
          // Find the property having the matching name and set the value
          PrivateAccessor<T>.DeclaredProperties.Single(info => info.Name.Equals(callerName)).SetValue(null, value);
        }
    
        /// <summary>
        /// Invokes a static async method with parameters and return no value.
        /// </summary>
        /// <param name="callerName">Name of the calling method.</param>
        /// <param name="parameter">The parameter to be passed.</param>
        protected static async Task InvokeStaticAsync([CallerMemberName] string callerName = null, params object[] parameter)
        {
          // Take the caller's name as method name
          await (Task) PrivateAccessor<T>.DeclaredMethods.Single(info => info.Name.Equals(callerName)).Invoke(null, parameter);
        }
    
        #endregion Protected Static Methods
    }

    此时,提供访问非public方法和属性的接入点的包装类MembersAccessWrapper修改如下:

    class MembersAccessWrapper : PrivateAccessor<Testee>
    {
        // other members
    
        #region Internal Properties
    
        /// <summary>
        /// Gives get / set access to the property "ProtecedProperty".
        /// </summary>
        internal string ProtectedProperty
        {
          get { return (GetPropertyValue<string>()); }
          set { SetPropertyValue(value); }
        }
    
        /// <summary>
        /// Gives get / set access to the property "PrivateProperty".
        /// </summary>
        internal string PrivateProperty
        {
          get { return (GetPropertyValue<string>()); }
          set { SetPropertyValue(value); }
        }
    
        #endregion Internal Properties
    
        #region Internal Static Properties
    
        /// <summary>
        /// Gives get / set access to the static property "PrivateStaticProperty".
        /// </summary>
        internal static string PrivateStaticProperty
        {
          get { return (GetStaticPropertyValue<string>()); }
          set { SetStaticPropertyValue(value); }
        }
    
        #endregion Internal Static Properties
    
        #region Internal Methods
    
        /// <summary>
        ///  Calls the protected method ProtectedMethod
        /// </summary>
        /// <param name="parameter">The parameter</param>
        /// <returns>The return value of the called method.</returns>
        internal string ProtectedMethod(string parameter)
        {
          // Use the base class to invoke the correct method.
          // Need to name the parameter, otherwise the first parameter will be interpreted as caller's name.
          return (Invoke<string>(parameter: parameter));
        }
    
        /// <summary>
        ///  Calls the private method PrivateMethodWithReturnValueAsync
        /// </summary>
        /// <param name="millisecondsDelay">Delaytime in milliseconds..</param>
        /// <param name="outputText">Output text.</param>
        /// <returns>Delaytime in seconds.</returns>
        internal async Task<double> PrivateMethodWithReturnValueAsync(int millisecondsDelay, string outputText)
        {
          // Invoke the method
          double returnValue = await InvokeAsync<double>(parameter: new object[] { millisecondsDelay, outputText });
    
          // Return the result.
          return (returnValue);
        }
    
        #endregion Internal Methods
    
    
        #region Internal Static Methods
    
        /// <summary>
        ///  Calls the private method PrivateMethodWithReturnValueAsync
        /// </summary>
        /// <param name="millisecondsDelay">Delaytime in milliseconds..</param>
        /// <param name="outputText">Output text.</param>
        /// <returns>Delaytime in seconds.</returns>
        internal static async Task PrivateStaticVoidMethodAsync(int millisecondsDelay, string outputText)
        {
          // Invoke the method
          await InvokeStaticAsync(parameter: new object[] { millisecondsDelay, outputText });
        }
    
        #endregion Internal Static Methods
    }

    测试代码略。

  • 相关阅读:
    java链接linux服务器,命令操作
    linux中php项目无法发送邮件:PEAR mail package is not installed
    linux下部署php项目-Apache、php、mysql关联
    MyEclipse黑色主题
    MyEclipse优化-六步攻略
    致产品
    万物归宗
    【产品渗透:移动端平台与纯应用APP产品的相互嵌入】
    【异类--一万小时定律】
    【活法】
  • 原文地址:https://www.cnblogs.com/sjjsxl/p/5130247.html
Copyright © 2020-2023  润新知