如何模拟静态单例?

时间:2010-01-12 17:21:31

标签: c# unit-testing rhino-mocks

我有很多课程,我被要求在Rhino Mocks中添加一些单元测试并遇到一些问题。

首先,我知道RhinoMocks不允许模拟静态成员。我正在寻找我的选择(除了使用TypeMock)。

我所拥有的课程示例与以下内容类似:

class Example1 : ISomeInterface
{
    private static ISomeInterface _instance;

    private Example1()
    {
        // set properties via private static methods
    }

    static Example1()
    {
        _instance = new Example1();
    }

    public static ISomeInterface Instance() 
    {
        get { return _instance; }
    }

    // Instance properties 

    // Other Instance Properties that represent objects that follow a similar pattern.
}

所以当我打电话给上面的课时,它看起来像这样......

Example1.Instance.SomeObject.GoDownARabbitHole();

我有办法在这种情况下模拟SomeObject.GoDownARabbitHole()或模拟实例吗?

7 个答案:

答案 0 :(得分:24)

受到这样的线索的劝阻,我花了很长时间才注意到,单身人士并不难以嘲笑。毕竟我们为什么要使用c#?

只需使用Reflection。

使用提供的示例代码,您需要确保在将静态字段设置为模拟对象之前调用静态构造函数。否则它可能会覆盖您的模拟对象。在设置测试之前,只需在单例上调用任何无效的东西。

ISomeInterface unused = Singleton.Instance();

System.Reflection.FieldInfo instance = typeof(Example1).GetField("_instance", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);

Mock<ISomeInterface> mockSingleton = new Mock<ISomeInterface>();
instance.SetValue(null, mockSingleton.Object);

我提供了使用Moq进行模拟的代码,但我猜Rhino Mocks非常相似。

答案 1 :(得分:11)

单身人士与可测试性不一致,因为他们很难改变。使用依赖注入将ISomeInterface实例注入到消费类中会更好:

public class MyClass
{
    private readonly ISomeInterface dependency;

    public MyClass(ISomeInterface dependency)
    {
        if(dependency == null)
        {
            throw new ArgumentNullException("dependency");
        }

        this.dependency = dependency;
    }

    // use this.dependency in other members
}

请注意Guard Claus与readonly关键字一起保证ISomeInterface实例始终可用。

这将允许您使用Rhino Mocks或其他动态模拟库将Test Doubles的ISomeInterface注入到消费类中。

答案 2 :(得分:6)

这是一种使用委托的低触摸方法,可以在运行时初始设置并更改。通过示例更好地解释(具体来说,模拟DateTime.Now):

http://www.lostechies.com/blogs/jimmy_bogard/archive/2008/11/09/systemtime-versus-isystemclock-dependencies-revisited.aspx

答案 3 :(得分:2)

图书中的示例Working Effectively with Legacy Code

要在测试工具中运行包含单例的代码,我们必须放松单例属性。这是我们如何做到的。第一步是向singleton类添加一个新的静态方法。该方法允许我们替换单例中的静态实例。我们称之为 的 setTestingInstance 即可。

public class PermitRepository
{
    private static PermitRepository instance = null;
    private PermitRepository() {}
    public static void setTestingInstance(PermitRepository newInstance)
    {
        instance = newInstance;
    }
    public static PermitRepository getInstance()
    {
        if (instance == null) 
        {
            instance = new PermitRepository();
        }
        return instance;
    }
    public Permit findAssociatedPermit(PermitNotice notice) 
    {
    ...
    }
...
}

现在我们有了这个setter,我们可以创建一个a的测试实例 PermitRepository并设置它。我们想在我们的测试设置中编写这样的代码:

public void setUp() {
PermitRepository repository = new PermitRepository();
...
// add permits to the repository here
...
PermitRepository.setTestingInstance(repository);
}

答案 4 :(得分:1)

您可以模拟界面ISomeInterface。然后,重构使用它的代码以使用依赖注入来获取对单例对象的引用。我在代码中多次遇到过这个问题,我最喜欢这个解决方案。

例如:

public class UseTheSingleton
{
    private ISomeInterface myX;

    public UseTheSingleton(ISomeInterface x)
    {
        myX = x;
    }

    public void SomeMethod()
    {
        myX.
    }
}

然后......

UseTheSingleton useIt = UseTheSingleton(Example1.Instance);

答案 5 :(得分:1)

查看Dependency Injection

你已经开始了这个,但是对于难以测试的类(静态等等),你可以使用adapter设计模式来编写一个包装这个难以测试代码的包装器。使用此适配器的interface,您可以单独测试代码。

对于任何单元测试建议和进一步的测试问题,请查看Google Testing Blog,特别是Misko的文章。

实例

你说你正在编写测试,所以可能为时已晚,但是你能否将静态重构为实例?或者是否有一个真正的理由说为什么所说的课程应该保持静止?

答案 6 :(得分:0)

您不必立即修复所有用途,只需要处理您现在正在处理的用途。将ISomeInterface字段添加到测试的类中,并通过构造函数设置它。如果你正在使用Resharper(你 使用Resharper,不是吗?),大部分内容都是微不足道的。如果这非常繁琐,你可以有多个构造函数,一个用于设置新的依赖项字段,另一个用单例作为默认值调用第一个构造函数。