在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
有时候我们写好的类库中,某些类的属性和方法不应该暴露出来,那么如何对这些非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> /// <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> /// <summary> #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() { } 这段测试代码仅测试了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">Delayt |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论