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现在返回的还是系统时间。 moles

我们试下使用Molas替换DateTime.Now的返回值,在测试项目引用列表中,右键MoleDomain,选择“Add Moles Assembly”,确定后会自动在项目中增加一个MoleDomain.moles文件,moles后缀的文件是让Moles对该程序集自动生成对应的Molas类型程序集,以便测试时使用。 moles 右键测试项目,选择“重新生成”,会发现程序自动引用了很多Moles相关的程序集,如Microsoft.Moles.Framework,还有自动生成的 MolaDomain.Moles程序集。 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
{
    .....
}

再次运行测试,终于通过测试了:) moles

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的方法,而ChildBase的子类:

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