模拟静态方法

90

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

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

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

这个静态方法 StaticClass.GetFile() 怎么被模拟?

附言:如果您有关于Moq和单元测试的阅读材料推荐,我会很感激。

8个回答

56

@Pure.Krome: 回答得很好,但我会补充一些细节。

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

  • TypeMock Isolator(可以模拟任何东西,但价格昂贵)
  • Telerik 的 JustMock(新加入的成员,价格较低但仍不免费)
  • Microsoft 的 Moles(唯一的免费隔离解决方案)

我推荐使用 Moles,因为它是免费的、高效的,并像 Moq 一样使用 lambda 表达式。只有一个重要的细节:Moles 提供的是 Stub 而不是 Mock。因此,你仍然可以使用 Moq 来处理接口和委托;)

Mock:一个实现接口并允许动态设置返回值/异常并提供检查特定方法是否被调用/未被调用的能力的类。
Stub:像 Mock 类一样,但它不提供验证方法是否已调用/未调用的能力。


11
这是最新的消息。Moles现在被称为Fakes,并且已经内置于Visual Studio 2012中:http://msdn.microsoft.com/en-us/library/hh549175.aspx - gscragg
2
在 Moles 中,您可以验证方法是否已被调用,只需添加一个布尔变量并在存根函数内将其设置为 true,也可以使用此解决方案验证调用中的所有参数。 - mart
3
您对模拟和存根的定义过于复杂。模拟是一个假对象,您断言某些行为已经发生。存根是一个假对象,仅用于向测试提供“固定”的数据。存根不会被断言。简而言之,模拟关注行为,存根关注状态。 - Scott Marcus
有一个开源替代品可以取代 Microsoft Fakes(仅在 Visual Studio 的高级/终极版本中提供),它叫做 Prig(PRototyping jIG):https://github.com/urasandesu/Prig。它可作为 NuGet 和可下载的二进制文件使用,只要不编译源代码,就可以在较低级别的 Visual Studio 版本中使用。 - Anders Asplund
3
Microsoft.Fakes不是“免费”的,因为它在VS社区版中不可用。 - Crono

37
Mocking框架(例如Moq或Rhinomocks)只能创建对象的模拟实例,这意味着无法模拟静态方法。
你也可以在此搜索 Google 以获得更多信息。
此外,此处之前有一些在StackOverflow上提出的问题:这里这里这里

21

在.NET中有一种可能性,可以排除MOQ和任何其他的mocking库。你需要在包含你想要mock的静态方法的程序集上,在解决方案资源管理器上右键单击,然后选择添加Fakes程序集。接下来,您可以自由地mock这个程序集的静态方法。

假设您想要mock System.DateTime.Now 静态方法。可以像这样进行:

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

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


18
警告:发现这篇文章的人需要知道,MS Fakes仅内置于Visual Studio 2012+ Premium/Ultimate版本中。你可能无法将其集成到持续集成链中。 - thomasb
3
这应该是被接受的答案。现在有了 MS Fakes 和 shims,很容易模拟静态方法。 - Jez
2
使用MS Fakes时要非常小心!Fakes是一个重定向框架,其使用很快会导致糟糕的架构。仅在绝对必要时使用Fakes,以拦截对第三方或(非常)遗留库的调用。最好的解决方案是创建一个带有接口的类包装器,然后通过构造函数注入或通过注入框架传递接口。模拟接口,然后将其传递到测试代码中。尽可能远离Fakes。 - Mike Christian

11

您可以使用从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
这当然有限制,比如我无法让它与通用静态方法一起工作。但是扩展方法肯定没有问题。 - mr100
姿态已经有一段时间没有更新了,而且有很多问题被报告了,这使得它现在不是一个好的选择。 - David Clarke

3

我很喜欢 Pose,但是它总是会抛出 InvalidProgramException,这似乎是一个已知的 问题。现在我使用 Smocks 如下所示:

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

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

2
我一直在尝试重构静态方法,使其调用可由外部设置的委托以进行测试。这不需要使用任何测试框架,是一个完全定制的解决方案,但是重构不会影响您的调用者签名,因此相对安全。
为了使此功能正常工作,您需要访问静态方法,因此它不适用于任何外部库,例如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);
}
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();
    }
}

1

0
现在您可以尝试我的免费库YT.IIGen。它可以生成静态类的接口和实现包装器。
using YT.IIGen.Attributes;

[IIFor(typeof(StaticClass), "StaticClassWrapper")]
internal partial interface IStaticClass
{
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接