模拟静态方法


73

最近,我开始使用Moq进行单元测试。我使用Moq来模拟不需要测试的类。

您通常如何处理静态方法?

public void foo(string filePath)
{
    File f = StaticClass.GetFile(filePath);
}

怎么会StaticClass.GetFile()嘲笑这个静态方法?

附言:对于您建议的最小起订量和单元测试,我将不胜感激。

Answers:



48

@ Pure.Krome:反应良好,但我会补充一些细节

@Kevin:您必须根据可以对代码进行的更改选择解决方案。
如果可以更改它,则进行一些依赖注入可以使代码更具可测试性。如果不能,则需要良好的隔离。
使用免费的模拟框架(Moq,RhinoMocks,NMock ...),您只能模拟委托,接口和虚拟方法。因此,对于静态,密封和非虚拟方法,您有3种解决方案:

  • TypeMock隔离器(可以模拟所有内容,但价格昂贵)
  • Telerik的JustMock(新手,价格较低,但仍然不是免费的)
  • Microsoft Moles(唯一的免费隔离解决方案)

我推荐Moles,因为它是免费,高效的,并使用像Moq这样的lambda表达式。只是一个重要的细节:les鼠提供存根,而不是模拟。因此,您仍然可以使用Moq作为接口和委托;)

Mock:实现接口的类,并允许动态设置值以返回/从特定方法抛出异常,并提供检查是否已调用/不调用特定方法的能力。
存根:与模拟类类似,不同之处在于它不提供验证方法是否已被调用的能力。


8
作为对此的更新。Moles现在称为Fakes,并内置于Visual Studio 2012中:msdn.microsoft.com/en-us/library/hh549175.aspx
gscragg 2013年

2
在Moles中,您可以验证是否仅通过添加一个布尔变量并将其设置为stub函数内部将其设置为true即可调用该方法,也可以使用此工作环境来验证调用中的所有参数。
集市

3
您对模拟和存根的定义比必要的更为复杂。模拟是一个伪造的对象,您可以断言某些行为已经发生。存根是伪造的对象,仅用于向测试提供“罐装”数据。永远不会主张存根。简而言之。模拟与行为有关,存根与状态有关。
user1739635 2015年

Microsoft Fakes(仅在Visual Studio的高级/最终版本中可用)有一个开源替代品,称为Prig(PRototyping jIG):github.com/urasandesu/Prig。它既可以使用nuget二进制文件,也可以下载可下载的二进制文件,并且只要您不编译源代码,就可以在Visual Studio的高级版本中使用它。
安德斯·阿斯普伦德

1
Microsoft.Fakes不是“免费”的,因为它在VS Community版本中不可用。
克鲁诺

18

.NET中有可能排除MOQ和任何其他模拟库。您必须在包含要模拟的静态方法的程序集上右键单击解决方案资源管理器,然后选择“添加假程序集”。接下来,您可以自由地模拟汇编静态方法。

假设您要模拟System.DateTime.Now静态方法。例如,以这种方式执行此操作:

using (ShimsContext.Create())
{
    System.Fakes.ShimDateTime.NowGet = () => new DateTime(1837, 1, 1);
    Assert.AreEqual(DateTime.Now.Year, 1837);
}

每个静态属性和方法都具有相似的属性。


9
警告发现此警告的人:MS Fakes仅内置在Visual Studio 2012+ Premium / Ultimate中。您可能无法将其集成到连续的集成链中。
thomasb 2015年

3
这应该是公认的答案。MS Fakes带有垫片,现在可以模拟静态方法。
Jez

1
使用MS Fakes时要非常小心!伪造品是一个重定向框架,其使用迅速导致体系结构不佳。仅在绝对必要时才使用Fakes来拦截对第三方或(非常)旧版库的调用。最好的解决方案是使用接口创建一个类包装器,然后通过构造函数注入或注入框架将接口传递进来。模拟接口,然后将其传递给测试中的代码。尽可能远离假货。
迈克·克里斯汀

10

您可以使用nuget提供的Pose库来实现。它使您可以模拟静态方法。在您的测试方法中编写以下代码:

Shim shim = Shim.Replace(() => StaticClass.GetFile(Is.A<string>()))
    .With((string name) => /*Here return your mocked value for test*/);
var sut = new Service();
PoseContext.Isolate(() =>
    result = sut.foo("filename") /*Here the foo will take your mocked implementation of GetFile*/, shim);

有关更多信息,请参见此处https://medium.com/@tonerdo/unit-testing-datetime-now-in-c-without-using-interfaces-978d372478e8


非常有前途,尽管当我尝试使用它时,我了解到它尚不支持扩展方法。希望它将很快包括在内。
Samer Adra

我也对它印象深刻。但是它已经适用于扩展方法,只需对其进行测试!您只需模拟扩展方法,就像它是一个普通的静态方法一样:Shim shim = Shim.Replace(()=> StaticClass.GetSomeNumber(Is.A <int>()))。With((int value)=> 2); 在这种情况下,GetSomeNumber是一种扩展方法,因此在代码中使用它:var number = 3.GetSomeNumber(); 这有效!
mr100 '18

当然这有局限性,例如我无法使其与通用静态方法一起使用。但是可以肯定的是扩展方法没有问题。
mr100

Pose已有一段时间没有更新,并且报告了许多问题,这些问题使其现在不适合使用。
大卫·克拉克

3

我喜欢Pose,但是无法停止抛出InvalidProgramException,这似乎是一个已知问题。现在,我使用这样的Smocks

Smock.Run(context =>
{
    context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));

    // Outputs "2000"
    Console.WriteLine(DateTime.Now.Year);
});

1

我一直在研究重构静态方法以调用委托的概念,您可以在外部为测试目的设置该委托。

这不会使用任何测试框架,而是完全定制的解决方案,但是重构不会影响您呼叫者的签名,因此这是相对安全的。

为此,您需要访问静态方法,因此它对于诸如的任何外部库均无效System.DateTime

这是我一直在玩的一个示例,其中创建了两个静态方法,一个方法的返回类型需要两个参数,而一个泛型则没有返回类型。

主要的静态类:

public static class LegacyStaticClass
{
    // A static constructor sets up all the delegates so production keeps working as usual
    static LegacyStaticClass()
    {
        ResetDelegates();
    }

    public static void ResetDelegates()
    {
        // All the logic that used to be in the body of the static method goes into the delegates instead.
        ThrowMeDelegate = input => throw input;
        SumDelegate = (a, b) => a + b;
    }

    public static Action<Exception> ThrowMeDelegate;
    public static Func<int, int, int> SumDelegate;

    public static void ThrowMe<TException>() where TException : Exception, new()
        => ThrowMeDelegate(new TException());

    public static int Sum(int a, int b)
        => SumDelegate(a, b);
}

单元测试(xUnit和应有)

public class Class1Tests : IDisposable
{
    [Fact]
    public void ThrowMe_NoMocking_Throws()
    {
        Should.Throw<Exception>(() => LegacyStaticClass.ThrowMe<Exception>());
    }

    [Fact]
    public void ThrowMe_EmptyMocking_DoesNotThrow()
    {
        LegacyStaticClass.ThrowMeDelegate = input => { };

        LegacyStaticClass.ThrowMe<Exception>();

        true.ShouldBeTrue();
    }

    [Fact]
    public void Sum_NoMocking_AddsValues()
    {
        LegacyStaticClass.Sum(5, 6).ShouldBe(11);
    }

    [Fact]
    public void Sum_MockingReturnValue_ReturnsMockedValue()
    {
        LegacyStaticClass.SumDelegate = (a, b) => 6;
        LegacyStaticClass.Sum(5, 6).ShouldBe(6);
    }

    public void Dispose()
    {
        LegacyStaticClass.ResetDelegates();
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.