在项目开发时,编写单元测试是一个很好的习惯,单元测试可以提升软件开发质量,提前发现代码中存在的BUG,看似浪费时间,其实节省了系统联调的时间,而且更容易定位和修复问题。
在C#开发过程中,我们一般会单独创建一个项目用于测试(测试项目),然后写大量测试用例对开发的模块(待测项目)进行测试。我常用的是MSTest,其命名空间Microsoft.VisualStudio.TestTools.UnitTesting。该工具同样适用于其他基于.Net平台的语言,用法类似。
测试项目是单独一个程序集,因此在待测项目中,我要需要将待测模块和接口都声明为public,但这种做法会破坏原有项目中的访问安全性。因此,我研究了如何在测试项目中访问被测项目中的非公有类和非公有方法。结论是可以使用测试工具提供的PrivateObject和PrivateType实现。
对于公有类,需要访问非公有方法、属性、字段时。首先定义一个PrivateObject,参数可以是一个实例化对象,或者指定类型和实例化参数。(我列举是常用方法,更多用法请访问文末的参考文献)
// public PrivateObject (object obj);
PrivateObject po = new PrivateObject(new MyClass());
// public PrivateObject (Type type, params object[] args);
PrivateObject po2 = new PrivateObject(typeof(MyClass), arg1, arg2)
如果该类为私有的,无法直接实例化对象,可以使用PrivateObject构造。前两个参数为程序集名(dll或exe的名,不带扩展名,在待测项目的属性中可以看到),完全限定名(命名空间也写上),最后是实例化参数,没有的可以不写。
// public PrivateObject (string assemblyName, string typeName, params object[] args);
PrivateObject po3 = new PrivateObject("DllName", "MyNamespace.MyClass", arg1, arg2);
然后使用Invoke调用非公有方法。
// public object Invoke (string name, params object[] args);
po.Invoke("FuncName", arg1, arg2);
其他的用法还有:
// 访问值
public object GetField (string name);
public object GetFieldOrProperty (string name);
public object GetProperty (string name, params object[] args);
public object GetArrayElement (string name, params int[] indices);
// 设置值
public void SetField (string name, object value);
public void SetFieldOrProperty (string name, object value);
public void SetProperty (string name, object value, params object[] args);
public void SetArrayElement (string name, object value, params int[] indices);
对于静态方法、静态字段,需要使用PrivateType访问
// 对于公有类
// public PrivateType (Type type);
PrivateType pt = new PrivateType(typeof(Myclass));
// 对于非公有类
// public PrivateType (string assemblyName, string typeName);
PrivateType pt2 = new PrivateType("DllName", "MyNamespace.MyClass");
访问其静态方法
// public object InvokeStatic (string name, params object[] args);
po.InvokeStatic("StaticFunc", arg1, arg2)
另外其静态字段、静态变量方法用法类似,可以自行探索。
写在最后:
无论是PrivateObject和PrivateType都使用了C#的反射机制,然后在UnitTesting命名空间中进行一次封装。而反射机制同样可以用于其他场景,动态调用一个DLL中的类方法,而不在开发时引用该DLL,这一系列接口在System.Reflection命名空间中。
参考文献: