警告:这个方法会有严重的性能下降,请自行斟酌使用
起因
在项目上写UnitTest时,感觉写起来不是很顺手,因为我们使用了Dependency Inject框架,所以代码里面有大量的protected
属性,这些是我们在写UT时没有必要测试的依赖项,需要被mock掉,我们的mock框架使用的是Moq
。由于是protected
的属性,无法直接在UT类里对需要mock的属性赋值,因此我们现在的做法是创建一个testable
类继承需要被测试的类,并且暴露出一个带参数的构造,来设置需要被mock的属性值,或者实现一个代理方法,来调用被测试类的private
或protected
方法。
被测试类大概长这样:
public class AClass : BaseClass
{
[Dependency]
protected object DependencyObject1 { get; set; }
[Dependency]
protected object DependencyObject2 { get; set; }
protected override void AMethod()
{
...
DependencyObject1.Method();
DependencyObject2.Method();
...
}
}
Testable类大概长这样:
public class AClassTestable : AClass
{
public AClassTestable(object do1, object do2)
{
this.DependencyObject1 = do1;
this.DependencyObject2 = do2;
}
public void CallBaseAMethod()
{
AMethod();
}
}
测试类大概长这样:
[TestFixture]
public class AClassTest
{
private Mock mockDo1;
private Mock mockDo2;
private AClassTestable target;
[SetUp]
public void Setup()
{
mockDo1 = new Mock();
mockDo2 = new Mock();
target = new AClassTestable(mockDo1.Object, mockDo2.Object);
}
[Test]
public void ShouldReturnXX()
{
...
target.CallBaseAMethod();
...
}
}
由于大量的protect
属性与方法存在,因此我们需要建立很多的代理类和代理方法来写UT,这是比较痛苦的,写一个UT的成本很高。
转机
后来我们想到了使用反射,来通过反射设置protected
或private
数据成员,并通过反射直接调用protected
或private
成员方法,这样就不用再写testable方法了,因此我们实现了对object的扩展方法,
public static class ObjectExtension
{
public static void SetNonPublicDataMember(this object obj, string memberName, object value)
{...}
public static void InvokeNonPublicMethod(this object obj, string name, param object[] args)
{...}
}
所以测试代码看起来像这样:
[TestFixture]
public class AClassTest
{
private Mock mockDo1;
private Mock mockDo2;
private AClass target;
[SetUp]
public void Setup()
{
mockDo1 = new Mock();
mockDo2 = new Mock();
target = new AClass();
target.SetNonPublicDataMember("DependencyObject1", mockDo1.Object);
target.SetNonPublicDataMember("DependencyObject2", mockDo2.Object);
}
[Test]
public void ShouldReturnXX()
{
...
target.InvokeNonPublicMethod("AMethod", (object[])null);
...
}
}
代码简洁了一些,但是感觉这种调用方式还不是特别的方便,所有的非public成员都需要用反射的方式来设置值和调用。
解决
为了让代码再简洁一些,于是思考有没有一种能够像使用public成员一样点吧点吧就能使用private成员的方法呢?然后我就找到了这个 dynamic + reflection 的方案。
先来看下效果:
注意AClass里面都是private的成员变量和方法,但是调用的时候却是使用a.Test
的形式,就好像调用public方法一样,是不是很帅气,只要拿这个叫做ObjectWrapper
的类包装一下,然后把类型声明为dynamic
,就可以愉快的点吧点吧啦。
再来看看ObjectWrapper
的实现:
主要原理是用到DynamicObject对象,使被包装对象拥有运行时的动态行为。另外覆盖了查找方法和属性的方法,使其能够绕过public private等访问限定符的限制
public class ObjectWrapper : DynamicObject
{
object _wrapped; //用于存储被包装的对象
static BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance
| BindingFlags.Static | BindingFlags.Public; //查找所有实例或静态的类成员
public ObjectWrapper(object o)
{
_wrapped = o;
}
//覆盖原有的调用成员方法,改用反射来查找成员方法
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
var types = args.Select(a => a.GetType());
var method = _wrapped.GetType().GetMethod(binder.Name, flags, null, types.ToArray(), null);
if (method != null)
{
result = method.Invoke(_wrapped, args);
return true;
}
return base.TryInvokeMember(binder, args, out result);
}
//覆盖默认的获取成员方法,改用反射来查找成员
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
//先查找属性
var prop = _wrapped.GetType().GetProperty(binder.Name, flags);
if (prop != null)
{
result = prop.GetValue(_wrapped);
return true;
}
//如果通过查找属性的方式没有找到,则按照字段来查找
var fld = _wrapped.GetType().GetField(binder.Name, flags);
if (fld != null)
{
result = fld.GetValue(_wrapped);
return true;
}
return base.TryGetMember(binder, out result);
}
//覆盖默认的获取成员方法,改用反射来查找成员
public override bool TrySetMember(SetMemberBinder binder, object value)
{
//先查找属性
var prop = _wrapped.GetType().GetProperty(binder.Name, flags);
if (prop != null)
{
prop.SetValue(_wrapped, value, null);
return true;
}
//如果通过查找属性的方式没有找到,则按照字段来查找
var fld = _wrapped.GetType().GetField(binder.Name, flags);
if (fld != null)
{
fld.SetValue(_wrapped, value);
return true;
}
return base.TrySetMember(binder, value);
}
}
优点:
使用方便,对源代码修改少,能够像public成员一样的调用形式来调用私有成员
缺点:
- 由于使用了dynamic类型声明被测对象,因此无法使用智能提示,所以需要自己注意调用的属性和方法
- 没有考虑方法重载的情况,在查找方法的时候没有判断参数类型,因此当有多个重名方法的时候只会调用第一个找到的方法
- public成员也走了反射的查找,并且调用了DLR,会有严重的性能问题。
性能测试
创建了4000个文件,每个测试文件3个测试,实现都一样,只是类名不一样,分别对testable和wrapper两种方法进行测试,测试结果如下图:
左边是wrapper的结果,右边是testable的结果,可以看到慢了将近3倍,这个速度实在不可接受,还是谨慎使用,看来方便也是有代价的。今天就到这吧。
测试完整代码:
Github
[amazon_link asins=’B015316YQE,B072NSJSTT,B01LW72R2M’ template=’CopyOf-ProductGrid’ store=’boyd-23′ marketplace=’CN’ link_id=”]
参考连接
DynamicObject
C# 4.0 AccessPrivateWrapper – Instantiate and Access Private/Internal Classes and Members via dynamic + reflection