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