Molas——.NET依赖分离框架
Moles是由微软研究院(Microsoft Research)开发的.NET依赖分离框架,它实现了使用自定义的委托(delegate)方法来替换原有类中的方法,以达到分离依赖,方便单元测试的目的。Moles在功能和用法上与开源的IoC框架Moq很像,但Moles有一些Moq实现不了功能,如替换静态方法,去掉静态构造函数,突破访问限制等。Molas非常有利于对ASP.NET WebForm构建的网站和依赖第三方类库的程序进行单元测试。
下载和安装
下载Moles后直接安装就可以了,里面集成有VS2010的插件,安装成功后,VS2010右键菜单中会集成Moles功能菜单。
使用示例
我们试下测试2000年千年虫的bug。在VS2010中创建一个MoleDomain的类项目,并创建类Y2KChecker,代码如下:
namespace MoleDomain { public static class Y2KChecker { public static void Check() { if (DateTime.Now == new DateTime(2000, 1, 1)) throw new ApplicationException("y2kbug!"); } } }
现在我们要测试这段代码,确定当时间为2000/1/1时,程序能正确抛出异常。很显然这段代码没法做单元测试,因为代码中的DateTime.Now是依赖系统时钟的,只返回当前时间,我们没法改变它的值使它刚好等于2000/1/1。怎么办好呢? 使用Molas解决这个问题很简单。创建一个测试项目,并引用MoleDomain项目,单元测试代码如下:
[TestMethod] [ExpectedException(typeof(ApplicationException))] public void Test() { Y2KChecker.Check(); }
运行测试,会显示预期的未通过,因为DateTime.Now现在返回的还是系统时间。
我们试下使用Molas替换DateTime.Now的返回值,在测试项目引用列表中,右键MoleDomain,选择“Add Moles Assembly”,确定后会自动在项目中增加一个MoleDomain.moles文件,moles后缀的文件是让Moles对该程序集自动生成对应的Molas类型程序集,以便测试时使用。
右键测试项目,选择“重新生成”,会发现程序自动引用了很多Moles相关的程序集,如Microsoft.Moles.Framework,还有自动生成的 MolaDomain.Moles程序集。
要使Moles正常运行,需要改下原来的单元测试代码。在测试方法上方加上HostType特性,并写下替换DateTime.Now返回值的代码:
[TestMethod] [ExpectedException(typeof(ApplicationException))] [HostType("Moles")] public void Test() { //利用委托替换原来的返回值 MDateTime.NowGet = () => new DateTime(2000, 1, 1); Y2KChecker.Check(); }
再次运行测试,发现还是失败,提示错误:
测试方法 MoleDomain.Test.Y2KCheckerTest.Test 引发了异常 Microsoft.Moles.Framework.Moles.MoleNotInstrumentedException,但应为异常 System.ApplicationException。异常消息: Microsoft.Moles.Framework.Moles.MoleNotInstrumentedException: The System.DateTime System.DateTime.get_Now() was not instrumented To resolve this issue, add the following attribute in the test project: using Microsoft.Moles.Framework; [assembly: MoledType(typeof(System.DateTime))]
提示缺少一些引用配置,在测试命名空间上方加上代码:
using Microsoft.Moles.Framework; [assembly: MoledType(typeof(System.DateTime))] namespace MoleDomain.Test { ..... }
再次运行测试,终于通过测试了:)
Mole基础知识
原始类成员方法对应的Mole类型属性如下:
- ◇ 静态方法表示为mole类型的静态属性
- ◇ 类实例方法表示为嵌套的AllInstances类型的静态属性
- ◇ 类构造函数表示为mole类型的命名为Constructor的静态属性
下面部分说明下如何使用. Static Methods 为mole类型的静态属性附加委托方法可以替换类静态方法的内容。mole类型属性只能附加一个委托方法。如MyClass类有一个静态方法MyMethod:
public static class MyClass { public static int MyMethod() { ... } }
我们附加一个mole到MyMethod中,使它一直返回5:
MMyClass.MyMethod = () =>5;
自动生成的MMyClass类型的代码结构如下:
public class MMyClass { public static Func MyMethod { set { ... } } }
安装Moles框架后,使用右键的“Add Moles Assembly”功能添加.mole后缀文件后,MMyClass就能自动生成。
实例方法(对所有实例生效)
和静态方法相似,也可以对所有实例方法进行mole。实例方法放置在嵌套类AllInstances的静态属性中,例如下面MyClass实例的MyMethod方法:
public class MyClass { public int MyMethod() { ... } }
mole一个方法使所有实例对象都返回5:
MMyClass.AllInstances.MyMethod = _ => 5;
自动生成的MMyClass结构如下:
public class MMyClass : MoleBase { public static class AllInstances { public static FuncMyMethod { set { ... } } } }
实例方法(对一个实例生效)
对不同的实例,实例方法可以mole不同的委托方法。mole的属性实际是mole类型实例自己的属性(不是静态方法),每个mole类型实例都会有一个原始类型的实例对象。如MyClass的实例方法MyMethod:
public class MyClass { public int MyMethod() { ... } }
我们可以创建两个MMyClass的实例对象,一个使它返回5,另一个使它返回10:
var myClass1 = new MMyClass() { MyMethod = () => 5 }; var myClass2 = new MMyClass() { MyMethod = () => 10 };
自动生成的mole类型代码结构如下:
public class MMyClass : MoleBase { public Func MyMethod { set { ... } } public MyClass Instance { get { ... } } }
原始类型对象可以通过mole实例对象的Instance属性获得:
var mole = new MMyClass(); var instance = mole.Instance;
mole实例对象也可以隐式转换为原始类型对象,所以你可以直接赋值对原始类型,如下:
var mole = new MMyClass(); MyClassinstance = mole;
构造函数(Constructors)
类构造函数也可以mole来进行一些赋值操作。类构造函数表示为mole类型的静态方法Constructor,如下面的MyClass类带有一个int参数的构造函数:
public class MyClass { public MyClass(int value) { this.Value = value; } ... }
通过附加构造函数使以后的所有实例的Value属性都返回-5:
MMyClass.ConstructorInt32 = (@this, value) => { var mole = new MMyClass(@this) { ValueGet = () => -5 }; };
如果你只想mole后面一个实例,我们只需把Constructor静态属性赋null值,如:
MMyClass.ConstructorInt32 = (@this, value) => { ... MMyClass.ConstructorInt32 = null; };
需要注意的是,每个mole类型都有两个构造函数,当需要一个新的mole实例对象时,使用默认的构造器;而带有一个原始类型参数的构造函数,只应该在mole构造函数时使用。
public MMyClass() { } public MMyClass(MyClass instance) : base(instance) { }
自动生成的MMyClass代码结构如下:
public class MMyClass : MoleBase { public static Action ConstructorInt32 { set { ... } } public MMyClass() { } public MMyClass(MyClass instance) : base(instance) { } ... }
基类成员(Base Members)
只要把子类实例作为基类构造函数的参数传入,就可以创建一个基类的mole对象,并访问到基类中的mole属性。例如,基类Base有一个MyMethod的方法,而Child是Base的子类:
public abstract class Base { public int MyMethod() { ... } } public class Child : Base { }
通过创建一个MBase对象我们能设置Base的mole属性:
var child = new MChild(); new MBase(child) { MyMethod = () => 5 };
注意这里,当MChild实例作为传入MBase构造函数时,会被隐式转换为Child实例。 MChild和MBase的自动生成代码如下:
public class MChild : MoleBase { public MChild() { } public MChild(Child child) : base(child) { } } public class MBase : MoleBase { public MBase(Base target) { } public Func MyMethod { set { ... } } }
静态构造函数
静态构造函数在Moles中被特殊对待,Moles只能简单地抹去静态构造函数,而不能重新为它附加新的委托方法。Moles通过指定[MolesEraseStaticConstructor]特性来抹去一个类的静态构造函数。
[assembly: MolesEraseStaticConstructor(typeof(MyStatic))] class MyStatic { static MyStatic() { throw new Exception(); // needs moling… } }
终结器(Finalizers)
对于终结器,Moles也是特殊对待的。Moles也是只能简单抹去终结器,通过指定[MolesEraseFinalizer]特性实现。
[assembly: MolesEraseFinalizer(typeof(MyFinalizer))] class MyFinalizer { ~MyFinalizer() { throw new Exception(); // needs moling… } }
私有方法
假如私有方法的签名类型是可见的,Moles会为私有方法自动生成mole属性。签名类型可见是指,参数类型或返回值类型是可见的,不是私有类型。
绑定接口
当类有实现接口时,Moles自动生成的mole类型会提供立即绑定接口成员的方法。例如,MyClass实现了s IEnumerable接口:
public class MyClass : IEnumerable { public IEnumerator GetEnumerator() { ... } ... }
通过mole类型的Bind方法,我们可以简捷地mole接口实现:
var myClass = new MMyClass(); myClass.Bind(new int[] { 1, 2, 3 });
自动生成的MMyClass代码结构如下:
public class MMyClass : MoleBase { public MMyClass Bind(IEnumerable target) { ... } }
Moles缺点
Moles缺点是,测试运行比较慢,还有测试代码只能在本机上才能测试通过,假如同伴获取代码后需要运行单元测试,必须也安装Molas环境。
参考资料
http://research.microsoft.com/en-us/projects/pex/molesmanual.pdf http://research.microsoft.com/en-us/projects/pex/documentation.aspx