【问题标题】:How to Mock a Static Singleton?如何模拟静态单例?
【发布时间】:2011-01-04 06:52:37
【问题描述】:

我有一些课程被要求添加一些单元测试到 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() 或模拟出实例?

【问题讨论】:

  • 您是否尝试过 Moq 而不是 Rhino?我相信它可以让你模拟静态方法。请参阅superexpert.com/blog/archive/2008/06/12/… 和适配器模式部分以模拟静态方法
  • 遗憾的是,改变模拟框架的决定不在我的掌控之中。我在一个庞大的代码库中,它在 Rhino 上是标准化的。至于使用适配器模式来模拟静态方法,同样可以使用 Rhino 来完成。我遇到的问题更多是通过静态方法创建单例,而不是测试静态方法本身。
  • Moq 不能模拟静态方法,而不遵循适配器模式。我应该补充一点,它比 Rhino 和其他模拟框架要好得多。

标签: c# unit-testing rhino-mocks


【解决方案1】:

书中的例子Working Effectively with Legacy Code

要在测试工具中运行包含单例的代码,我们必须放宽单例属性。这是我们如何做到的。第一步是向单例类添加一个新的静态方法。该方法允许我们替换单例中的静态实例。我们称之为 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) 
    {
    ...
    }
...
}

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

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

【讨论】:

  • 伙计,我喜欢你的建议有两个原因:简单和现实。我今天正在研究它,因为我在实施测试时开始遇到单例问题。即使没有读过这本书,我也实现了与您提出的解决方案类似的解决方案(它在我的“阅读”列表中)。我感到不舒服,因为我发现的大多数答案和文章给出的答案充其量只是简单化(至少对于具有 60K 行代码的企业应用程序而言)。现在,我更放心的是,有人尊敬的人在书中写了这个。毕竟是 Michael Feathers 建议的。
  • @Teoman shipahi:我喜欢这个想法,但是如果构造函数是private(这对于单身人士来说很常见),你怎么能在测试中调用new PermitRepository()
  • @GyörgyBalássy 编辑:我相信作者打算在设置中使用 PermitRepository 本身就是一个模拟类。 books.google.com/…
【解决方案2】:

对这样的线程感到沮丧,我花了很长时间才注意到,单例并不难模拟。毕竟我们为什么要使用 c#?

只需使用反射。

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

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 非常相似。

【讨论】:

  • 刚刚在工作中使用了它,并让某人重回正轨。非常感谢。
  • 问题是大多数单例不是基于接口的。我会说这非常罕见。如果有接口,那么最好不要使用单例,而是使用依赖注入。
  • 哇,这个解决方案值得重构​​我的单例来实现接口。
  • 要添加的一项。一旦它被模拟,你的其他测试也将使用模拟。你可能不想要那个。在测试结束时,您应该使用以下方法删除模拟:instance.SetValue("_instance", null);
【解决方案3】:

您不必一次修复所有用途,只需修复您现在正在处理的那个。在被测类中添加一个 ISomeInterface 字段,并通过构造函数进行设置。如果您使用的是 Resharper(您正在使用 Resharper,不是吗?),大部分操作都将是微不足道的。如果这真的很繁琐,你可以有多个构造函数,一个设置新的依赖字段,另一个调用第一个以单例作为默认值的构造函数。

【讨论】:

    【解决方案4】:

    查看Dependency Injection

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

    有关任何单元测试建议和进一步的测试问题,请查看Google Testing Blog,尤其是 Misko 的文章。

    实例

    您说您正在编写测试,所以可能为时已晚,但是您可以将静态重构为实例吗?或者说这个类应该保持静态是否有真正的原因?

    【讨论】:

    • 我在类定义下给出的示例用法行用于可能接近 100 个或更多的文件。我实际上是在尝试测试一个调用这个静态单例类的类,如果我可以模拟它,这很简单。因此,将其转换为实例类而不是静态单例的工作负载会很危险。
    • 这很公平。在这种情况下,DI 将是要走的路。祝测试愉快。
    【解决方案5】:

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

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

    【讨论】:

    • 适配器模式的非常有趣的方法。谢谢
    【解决方案6】:

    单例与可测试性不一致,因为它们很难改变。您最好使用 Dependency Injection 将 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 或其他动态模拟库将 ISomeInterface 的 Test Doubles 注入消费类。

    【讨论】:

    • 依赖注入有时会显着增加代码复杂度。想象一下,您有一个环境依赖项,应该可以从代码中的所有位置轻松访问它。 static instance 是更好的选择,否则你只会污染所有的构造函数。
    • 其中一个依赖项是本地化程序,它应该翻译您的字符串。它显然会访问一些文件或数据库来加载翻译,你必须模拟它。模拟静态实例并非不可能。在每个班级中注入定位器简直是丑陋的!
    【解决方案7】:

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

    例如:

    public class UseTheSingleton
    {
        private ISomeInterface myX;
    
        public UseTheSingleton(ISomeInterface x)
        {
            myX = x;
        }
    
        public void SomeMethod()
        {
            myX.
        }
    }
    

    然后……

    UseTheSingleton useIt = UseTheSingleton(Example1.Instance);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-21
      相关资源
      最近更新 更多