在编写NUnit测试的时候有时候需要对非公有有成员进行测试,每个方法都通过反射还是比较麻烦而且不美观。在4.0中可以借助DLR创建一个访问器Accessor动态获取私有成员直接调用。
动态语言运行时 (DLR) 是一种新运行时环境,它将一组适用于动态语言的服务添加到 CLR。借助于 DLR,可以更轻松地开发要在 .NET Framework 上运行的动态语言,而且向静态类型化语言添加动态功能也会更容易。
假设有一个类Class1
public class Class1
{
private int Add(int a, int b)
{
return a + b;
}
}
我希望能够这样编写测试
[Test]
public void Class1AddTest()
{
dynamic class1 = new Accessor(typeof(Class1));
int result = class1.Add(1, 2);
Assert.That(result, Is.EqualTo(3));
}
现在我设计了一个Accessor类来完成这个工作 .
下面是我的实现方式:
首先要继承动态行为基类DynamicObject,先定义成员包括被代理的实例、实例包含的方法表和属性表
/// <summary>
/// 实例访问器
/// </summary>
public sealed class Accessor : DynamicObject
{
#region 成员
/// <summary>
/// 实例
/// </summary>
private readonly object instance;
/// <summary>
/// 方法表
/// </summary>
private readonly IDictionary<string, IGrouping<string, MethodInfo>> methodInfos;
/// <summary>
/// 属性表
/// </summary>
private readonly IDictionary<string, PropertyInfo> propertyInfos;
#endregion
}
为了实现成员访问必须实现对方法Method、属性Property,和索引器Index的访问
首先是方法Method,
方法调用有一个重要的问题就是方法重载overload的解决
使用方法名作为Key用 Dictionary<string, IGrouping<string, MethodInfo>>来分组保存重载的方法
方法表和属性表通过instance的Type反射出来,不过要注意包含私有方法。
public Accessor(object instance)
{
this.instance = instance;
const BindingFlags flag = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var type = instance.GetType();
this.methodInfos = type.GetMethods(flag)
.GroupBy(p => p.Name)
.ToDictionary(p => p.Key);
this.propertyInfos = type.GetProperties(flag)
.ToDictionary(p => p.Name);
}
查找重载方法主要通过对比参数表实现,具体如下:
private static bool CheckOverLoadParameters(MethodInfo method, object[] args)
{
var parms = method.GetParameters();
var length = parms.Length;
if (length == 0 && args == null) return true;
if (length != args.Length) return false;
for (int i = 0; i < length; i++)
{
if (parms[0].ParameterType != args[0].GetType())
return false;
}
return true;
}
下面要做的就是重写DynamicObject的访问方法bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) 返回值true表示此方法可用。
查询到对于的MethodInfo,通过反射调用即可,具体实现如下:
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
IGrouping<string, MethodInfo> methods;
bool success = methodInfos.TryGetValue(binder.Name, out methods)
if (success)
{
var method = methods.Where(p => CheckOverLoadParameters(p, args)).FirstOrDefault();
if (method != null)
{
result = method.Invoke(instance, args);
return true;
}
}
return base.TryInvokeMember(binder, args, out result);
}
属性不能重名就比较简单了,直接从属性表获取即可
属性Get操作需要重写DynamicObject的访问方法TryGetMember,Set操作需要重写DynamicObject的访问方法TrySetMember。
我们都知道XXX的Get操作实际上是调用get_XXX(),所以反射调用的时候需要从propertyInfo的GetGetMethod获取Get操作的MethodInfo
Get操作的具体实现如下,Set操作只是将GetGetMethod换成GetSetMethod。
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
PropertyInfo propertyInfo;
bool success = propertyInfos.TryGetValue(binder.Name, out propertyInfo);
if (success)
{
var method = propertyInfo.GetGetMethod(true);
if (method != null)
{
result = method.Invoke(instance, null);
return true;
}
}
return base.TryGetMember(binder, out result);
}
最后是索引器了,了解CLR的都知道索引器其实是get_Item属性,那么在属性表查询属性名为"Item"的即可。
索引器Get操作需要重写DynamicObject的访问方法TryGetIndex,Set操作需要重写DynamicObject的访问方法TrySetIndex。
代码和属性操作差不多,只是反射的时候直接调用propertyInfo的SetValue和GetValue。
索引器Get操作具体实现如下:
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
PropertyInfo propertyInfo;
bool success = propertyInfos.TryGetValue("Item", out propertyInfo);
if (success)
{
result = propertyInfo.GetValue(instance, indexes);
return true;
}
return base.TryGetIndex(binder, indexes, out result);
}
好了这样就可以调用了,测试一下:
public class Class1
{
private int Add(int a, int b)
{
return a + b;
}
private int Add(int a, int b, int c)
{
return a + b + c;
}
private int X { get; set; }
public int this[int i]
{
get { return tmp[i]; }
set { tmp[i] = value; }
}
private readonly int[] tmp = new int[100];
}
[TestFixture]
public class Class1Test
{
[Test]
public void AddTest()
{
dynamic class1 = new Accessor(typeof(Class1));
int result = class1.Add(1, 2);
Assert.That(result, Is.EqualTo(3));
int result = class1.Add(1, 2, 3);
Assert.That(result, Is.EqualTo(6));
}
[Test]
public void XTest()
{
dynamic class1 = new Accessor(typeof(Class1));
class1.X = 1;
Assert.That(class1.X, Is.EqualTo(1));
}
[Test]
public void IndexIntTest()
{
dynamic class1 = new Accessor(typeof(Class1));
class1[0] = 1;
Assert.That(class1[0], Is.EqualTo(1));
}
}
这样测试私有成员就方便多了。
Accessor完整代码如下:
/// <summary>
/// 实例访问器
/// </summary>
public sealed class Accessor : DynamicObject
{
#region 成员
/// <summary>
/// 实例
/// </summary>
private readonly object instance;
/// <summary>
/// 方法表
/// </summary>
private readonly IDictionary<string, IGrouping<string, MethodInfo>> methodInfos;
/// <summary>
/// 属性表
/// </summary>
private readonly IDictionary<string, PropertyInfo> propertyInfos;
#endregion
#region 构造函数
/// <summary>
/// 通过实例创建实例访问器
/// </summary>
/// <param name="instance">实例对象</param>
public Accessor(object instance)
{
this.instance = instance;
const BindingFlags flag = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var type = instance.GetType();
this.methodInfos = type.GetMethods(flag)
.GroupBy(p => p.Name)
.ToDictionary(p => p.Key);
this.propertyInfos = type.GetProperties(flag)
.ToDictionary(p => p.Name);
}
/// <summary>
/// 通过指定实例类型的默认构造创建实例访问器
/// </summary>
/// <param name="type">实例类型</param>
public Accessor(Type type)
: this(Activator.CreateInstance(type))
{
}
/// <summary>
/// 通过指定实例类型创建实例访问器
/// </summary>
/// <param name="type">实例类型</param>
/// <param name="args">参数</param>
public Accessor(Type type, params object[] args)
: this(Activator.CreateInstance(type, args))
{
}
/// <summary>
/// 通过指定实例类型名创建实例访问器
/// </summary>
/// <param name="assemblyName">程序集名称</param>
/// <param name="fullTypeName">实例类型全名</param>
/// <param name="args">参数</param>
public Accessor(string assemblyName, string fullTypeName, params object[] args)
: this(Activator.CreateInstance(
assemblyName, fullTypeName, false,
BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic,
null, args, null, null).Unwrap())
{
}
/// <summary>
/// 通过指定实例类型名的默认构造函数创建实例访问器
/// </summary>
/// <param name="assemblyName">程序集名称</param>
/// <param name="fullTypeName">实例类型全名</param>
public Accessor(string assemblyName, string fullTypeName)
: this(assemblyName, fullTypeName, null)
{
}
/// <summary>
/// 检查方法重载
/// </summary>
/// <param name="method"></param>
/// <param name="args"></param>
/// <returns></returns>
private static bool CheckOverLoadParameters(MethodInfo method, object[] args)
{
var parms = method.GetParameters();
var length = parms.Length;
if (length == 0 && args == null) return true;
if (length != args.Length) return false;
for (int i = 0; i < length; i++)
{
if (parms[0].ParameterType != args[0].GetType())
return false;
}
return true;
}
#endregion
#region DynamicObject 访问方法重写
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
IGrouping<string, MethodInfo> methods;
bool success = methodInfos.TryGetValue(binder.Name, out methods);
if (success)
{
var method = methods.Where(p => CheckOverLoadParameters(p, args)).FirstOrDefault();
if (method != null)
{
result = method.Invoke(instance, args);
return true;
}
}
return base.TryInvokeMember(binder, args, out result);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
PropertyInfo propertyInfo;
bool success = propertyInfos.TryGetValue(binder.Name, out propertyInfo);
if (success)
{
var method = propertyInfo.GetGetMethod(true);
if (method != null)
{
result = method.Invoke(instance, null);
return true;
}
}
return base.TryGetMember(binder, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
PropertyInfo propertyInfo;
bool success = propertyInfos.TryGetValue(binder.Name, out propertyInfo);
if (success)
{
var method = propertyInfo.GetSetMethod(true);
if (method != null)
{
method.Invoke(instance, new[] { value });
return true;
}
}
return base.TrySetMember(binder, value);
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
PropertyInfo propertyInfo;
bool success = propertyInfos.TryGetValue("Item", out propertyInfo);
if (success)
{
result = propertyInfo.GetValue(instance, indexes);
return true;
}
return base.TryGetIndex(binder, indexes, out result);
}
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
{
PropertyInfo propertyInfo;
bool success = propertyInfos.TryGetValue("Item", out propertyInfo);
if (success)
{
propertyInfo.SetValue(instance, value, indexes);
return true;
}
return base.TrySetIndex(binder, indexes, value);
}
#endregion
}