65.9K
CodeProject 正在变化。 阅读更多。
Home

嘲讽不可嘲讽:在 Gallio 中使用 Microsoft Moles

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2010 年 8 月 1 日

CPOL

6分钟阅读

viewsIcon

11745

更新 29/04/10:与我最初在这篇博文中声明的相反,Moles 不仅作为 Pex 框架的一部分可用,而且还可以在 Visual Studio Gallery 上独立使用,并且完全免费(无需 MSDN 订阅)。 - 感谢 Peli 的更正……


更新 29/04/10:

与我最初在这篇博文中声明的相反,Moles 不仅作为 Pex 框架的一部分可用,而且还可以在 Visual Studio Gallery 上独立使用,并且完全免费(无需 MSDN 订阅)。 - 感谢 Peli 的更正……


 

通常的开源模拟框架(例如 MoqRhino.Mocks)只能模拟接口和虚方法。与此相反,Microsoft 的 Moles 框架可以‘模拟’几乎任何东西,因为它使用运行时插桩在被模拟方法的 MSIL 主体中注入回调。因此,可以拦截任何 .NET 方法,包括密封类型中的非虚/静态方法。当处理调用 .NET 框架、某些第三方或遗留代码等的代码时,这可能会非常有帮助……

可以在我的 Delicious 工具箱的此链接中找到一些有用的收集资源(指向网站、文档材料和一些视频):

http://delicious.com/thomasweller/toolbox+moles

Moles 的 Gallio 扩展

最初,Moles 是 Microsoft Pex 框架的一部分,因此与 Visual Studio Unit Tests (MSTest) 集成得最好。然而,Moles 示例下载包含一些额外的程序集,也支持其他单元测试框架。它们提供了一个Moled特性,以简化与相应框架的 mole 类型的用法(示例中包含 NUnit、xUnit.net 和 MbUnit v2 的扩展)。由于没有适用于 Gallio 平台的此类扩展,我亲自动手编写了几行代码 - 产生的Gallio.Moles.dll包含在示例下载中。

有了这个小程序集,就可以像这样在 Gallio 中使用 Moles 了

[Test, Moled]
public void SomeTest()
{
    ...

你可以用它做什么

如果你需要‘模拟’除虚方法或实现接口的方法之外的东西,Moles 会非常有帮助。这可能是在处理某些第三方组件、遗留代码时,或者如果你想‘模拟’ .NET 框架本身。

通常,你需要使用MoledType特性在程序集级别声明要使用的每个被模拟类型。例如

[assembly: MoledType(typeof(System.IO.File))]

以下是一些 Moles 的典型用例。有关更详细的概述(包括命名约定和创建所需 moles 程序集的说明),请参阅上面的参考资料。 

    拦截 .NET 框架

    想象一下,你想测试一个如下所示的方法,该方法内部调用了一个框架方法: 

    public void ReadFileContent(string fileName)
    {
        this.FileContent = System.IO.File.ReadAllText(fileName);
    }

    使用 mole,你会像这样用一个运行时委托替换对File.ReadAllText(string)方法的调用

    [Test, Moled]
    [Description("This 'mocks' the System.IO.File class with a custom delegate.")]
    public void ReadFileContentWithMoles()
    {
        // arrange ('mock' the FileSystem with a delegate)
        System.IO.Moles.MFile.ReadAllTextString = 
            (fname => fname == FileName ? FileContent : "WrongFileName");
     
        // act 
        var testTarget = new TestTarget.TestTarget();
        testTarget.ReadFileContent(FileName);
     
        // assert 
        Assert.AreEqual(FileContent, testTarget.FileContent);
    }

     

    拦截静态方法和/或类

    像下面这样的静态方法……

    public static string StaticMethod(int x, int y)
    {
        return string.Format("{0}{1}", x, y);
    }

    ……可以用以下方式‘模拟’

    [Test, Moled]
    public void StaticMethodWithMoles()
    {
        MStaticClass.StaticMethodInt32Int32 = ((x, y) => "uups");
     
        var result = StaticClass.StaticMethod(1, 2);
     
        Assert.AreEqual("uups", result);
    }

     

    拦截构造函数

    你甚至可以用类构造函数执行这种委托操作。它的语法不是很直观,因为你必须设置 mole 的内部状态,但总的来说它运行得非常好。例如,要替换这个构造函数……

    public class ClassWithCtor
    {
        public int Value { get; private set; }
     
        public ClassWithCtor(int someValue)
        {
            this.Value = someValue;
        }
    }

    ……你会这样做

    [Test, Moled]
    public void ConstructorTestWithMoles()
    {
        MClassWithCtor.ConstructorInt32 =
               ((@class, @value) => new MClassWithCtor(@class) {ValueGet = () => 99});
     
        var classWithCtor = new ClassWithCtor(3);
     
        Assert.AreEqual(99, classWithCtor.Value);
    }

     

    拦截抽象基类

    你也可以使用这种方法来‘模拟’你在测试中调用的类的抽象基类。假设你有这样的东西

    public abstract class AbstractBaseClass
    {
        public virtual string SaySomething()
        {
            return "Hello from base.";
        }
    }  
     
    public class ChildClass : AbstractBaseClass
    {
        public override string SaySomething()
        {
            return string.Format(
                "Hello from child. Base says: '{0}'", 
                base.SaySomething());
        }
    }

    然后你会像这样设置子类的底层基类

    [Test, Moled]
    public void AbstractBaseClassTestWithMoles()
    {
        ChildClass child = new ChildClass();
        new MAbstractBaseClass(child)
            {
                    SaySomething = () => "Leave me alone!"
            }
            .InstanceBehavior = MoleBehaviors.Fallthrough;
     
        var hello = child.SaySomething();
     
        Assert.AreEqual("Hello from child. Base says: 'Leave me alone!'", hello);
    }

    将 moles 的行为设置为MoleBehaviors.Fallthrough如果未显式提供相应的委托,则会导致调用‘原始’方法 - 在这里,它会导致ChildClass’覆盖的SaySomething()方法被调用。

    Moles 框架可能在许多场景中都很有帮助(例如,它也可以拦截接口实现,如IEnumerable<T>等等……)。我想到的另一个可能性是(因为我目前正在处理它),是用委托替换仓库类对 ADO.NET Entity Framework O/R 映射器的调用,以将仓库类与底层数据库隔离开,否则这将是不可能的……

    用法

    由于 Moles 依赖于运行时插桩,因此 mole 类型必须在 Pex 探查器下运行。如果你使用 MSTest (Visual Studio Unit Test) 编写测试,这在 Visual Studio 中才有效。虽然通常可以使用其他单元测试框架与 Moles 一起使用,但它们要求通过命令行运行相应的测试,通过moles.runner.exe工具执行。典型的测试执行将类似于此

    moles.runner.exe <mytests.dll> /runner:<myframework.console.exe> /args:/<myargs>

    因此,被模拟的测试可以通过 NCover 等工具或脚本工具(如 MSBuild)运行(这使得它们易于在持续集成环境中运行),但它们在常规 TDD 工作流程中(我在此详细描述过)运行起来有些不方便。为了让这个过程更流畅一些,我编写了一个 ReSharper 实时模板来生成相应的命令行(它也包含在示例下载中 -moled_cmd.xml)。 - 这只是一个快速简单的‘解决方案’。也许编写一个额外的 Gallio 适配器插件(类似于已提供的许多其他插件)并将其包含在 Gallio 下载包中是有意义的,如果对此有足够的需求的话。

    目前,从 Visual Studio 中运行带有 Moles 框架的测试的唯一方法是将其与 MSTest 一起使用。从命令行,任何带有托管控制台运行器的东西都可以使用(前提是已安装相应的扩展)……

    一个典型的 Gallio/Moles 命令行(由提到的 R# 模板生成)看起来像这样

    "%ProgramFiles%\Microsoft Moles\bin\moles.runner.exe" /runner:"%ProgramFiles%\Gallio\bin\Gallio.Echo.exe" "Gallio.Moles.Demo.dll" /args:/r:IsolatedAppDomain /args:/filter:"ExactType:TestFixture and Member:ReadFileContentWithMoles"

    -- 注意:在使用 Echo(Gallio 的控制台运行器)的命令行时,请务必始终包含IsolatedAppDomain 选项,否则测试将无法使用插桩回调! --

    许可问题

    正如我已经说过的,免费的模拟框架只能模拟接口和虚方法。如果你想模拟其他东西,你需要 Typemock Isolator 工具,它需要支付许可费用(尽管与此类工具为软件项目带来的价值相比,这些‘成本’非常低,但在现实生活中,花钱通常是一个相当大的障碍)。Moles 框架也不是完全免费的,但它与(密切相关的)Pex 框架具有相同的许可条件:仅限学术/非商业用途免费,要在‘真实’软件项目中使用它需要 MSDN 订阅(从 VS2010pro 开始)。

    演示解决方案

    示例解决方案(VS 2008)可在此处下载。它包含Gallio.Moles.dll提供此处所述的Moled特性,以及上述 R# 模板(moled_cmd.xml) 和包含上述用例场景的测试夹具。要运行它,你需要安装 Gallio 框架(下载)和 Microsoft Moles(下载)在默认位置。

    祝测试愉快……

     

    kickit
    shoutit
    <iframe width="150" height="20" frameborder="0" scrolling="no" style="position: relative; top: 4px;" id="dzoneframe" title="Zone it!" src="http://widgets.dzone.com/links/widgets/zoneit.html?t=2&url=&title="></iframe>
    delicious facebook digg reddit linkedin stumbleupon technorati mrwong yahoo google-48x48 twitter email favorites

 

<iframe src="http://ads.geekswithblogs.net/a.aspx?ZoneID=5&Task=Get&PageID=31016&SiteID=1" width=1 height=1 Marginwidth=0 Marginheight=0 Hspace=0 Vspace=0 Frameborder=0 Scrolling=No> <script language='javascript1.1' src="http://ads.geekswithblogs.net/a.aspx?ZoneID=5&Task=Get&Browser=NETSCAPE4&NoCache=True&PageID=31016&SiteID=1"></script> <noscript> </noscript> </iframe>
© . All rights reserved.