【问题标题】:Unit testing object construction/initialization单元测试对象构造/初始化
【发布时间】:2011-03-09 00:29:45
【问题描述】:

我有一个类 Foo,它使用另一个类 Bar 来做一些事情。

我正在尝试进行测试驱动开发,因此正在为 Foo 编写单元测试以确保它在 Bar 上调用适当的方法,为此我正在使用依赖注入和模拟 Bar(使用 Rhino Mocks) .

例如(在 C# 中):

class Foo
{
  private IBar bar= null;

  public Foo(IBar bar)
  {
    this.bar= bar;
  }

  public void DoSomething()
  {
    bar.DoIt();
  }
}

class FooTests
{
  ...
  void DoSomethingCallsBarsDoItMethod()
  {
    IBar mockBar= MockRepository.GenerateMock<IBar>();
    mockBar.Expect(b=>b.DoIt());    
    Foo foo= new Foo(mockBar);
    foo.DoSomething();
    mockBar.VerifyAllExpectations();
  }
}

这一切似乎都很好,但我实际上希望 Bar 配置一个特定的参数,并且打算通过 Foo 的构造函数传入这个参数。 例如。 (在 C# 中):

public Foo(int x)
{
  this.bar = new Bar(x);
}

我不确定哪种方法可以将其更改为更易于测试。我能想到的一种选择是将 Bar 的初始化移出其构造函数,例如

public Foo (IBar bar, int x)
{
  this.bar= bar;
  this.bar.Initialize(x);
}

我觉得这让 Bar 更难使用。

我怀疑可能会有某种解决方案涉及使用配置为向 Foo 注入模拟 IBar 的 IoC 容器,并提供对创建的模拟的访问以进行期望验证,但我认为这会使 Foo 变得不必要地复杂。我只使用依赖注入来模拟依赖项进行测试,因此目前不使用 IoC 容器,只是在默认构造函数的链式构造函数调用中构造依赖项(尽管我意识到这会创建更多未经测试的代码)例如

public Foo() :
  this(new Bar())
{
}

谁能推荐测试依赖对象构造/初始化的最佳方法?

【问题讨论】:

    标签: c# unit-testing dependency-injection mocking


    【解决方案1】:

    在我看来,您让 实现细节通过 API 设计泄漏。 IBar 应该对 Foo 及其要求一无所知。

    使用 构造函数注入 实现 Bar,就像使用 Foo 一样。您现在可以通过从外部提供实例来让它们共享同一个实例:

    var x = 42;
    IBar bar = new Bar(x);
    Foo foo = new Foo(bar, x);
    

    这将要求您的 Foo 构造函数如下所示:

    public class Foo
    {
        private readonly int x;
        private readonly IBar bar;
    
        public Foo(IBar bar, int x)
        {
            if (bar == null)
            {
                throw new ArgumentNullException("bar");
            }
    
            this.bar = bar;
            this.x = x;
        }
    }
    

    这允许 Foo 处理IBar 的任何实现,而不会让实现细节泄漏。将IBarx 分开可以看作是Interface Segregation Principle 的实现。

    在 DI Container 术语中,x 可以被视为 单例范围(不要与单例设计模式混淆),因为在许多应用程序中只有一个 x 实例不同的组件。该实例是共享的,它严格来说是生命周期管理问题 - 不是 API 设计问题。

    大多数 DI 容器都支持将服务定义为单例,但不需要 DI 容器。如上例所示,您还可以手动连接共享服务。

    在您直到运行时才知道x 的值的情况下,Abstract Factory is the universal solution

    【讨论】:

    • Foo 不关心 x,除了 Bar 必须用它初始化这一事实。我已经编辑了我的问题以澄清这一点。谢谢。
    • @Ergwun:如果 Foo 不关心 x,那么只需使用 x 创建 Bar 并将其注入 Foo。如果 x 在运行时才能知道,请使用抽象工厂在 Foo 中创建 IBar - 请参阅我答案的最后一个链接。
    • 在尝试了一段时间后,我发现抽象模式工厂的这种使用对于这类问题非常有帮助。谢谢!
    【解决方案2】:

    在你的依赖注入框架中定义这个变量 x,并将它注入到 Foo 和 Bar。

    【讨论】:

      【解决方案3】:

      Foo 是如何创建的?它来自控制容器的反转吗?如果是这样,您可以使用 IoC 容器中的功能来配置这些对象。

      如果没有,那就硬着头皮添加一个方法 Foo.Initializer(parameter) 调用 Bar.Initialize(parameter) 或者可能采用 Bar 的实例。

      我做单元测试的时间没有你那么长,但我发现我不再在构造函数中做事了。它只是妨碍测试。构造函数获取对象依赖项,然后完成。

      【讨论】:

      • Foo 目前不是来自 IoC 容器。单独的初始化器解决方案是我目前一直倾向于使用的解决方案。另一个缺点是必须在所有其他公共方法中添加断言和/或异常,以防止使用未初始化的对象,如果构造函数可以进行初始化,则不需要这样做。谢谢。
      【解决方案4】:

      这更多是关于设计的问题。为什么 Foo 需要用特定的 x 值初始化 Bar?可能没有最佳地定义类边界。也许Bar根本不应该封装x的值,只是把它作为方法调用中的参数:

      class Foo
      {
        private IBar bar= null;
        private int x;
      
        public Foo(IBar bar, int x)
        {
          this.bar= bar;
          this.x = x;
        }
      
        public void DoSomething()
        {
          bar.DoIt(x);
        }
      }
      

      如果由于某种原因你不能改变设计,我想我会在构造函数中有一个断言:

      class Foo
      {
        private IBar bar= null;
      
        public Foo(IBar bar)
        {
          if(bar.X != 42) 
          {
            throw new ArgumentException("Expected Bar with X=42, but got X="+bar.X);
          }
          this.bar= bar;
        }
      
        public void DoSomething()
        {
          bar.DoIt();
        }
      }
      

      这个例子还表明您需要进行一些集成测试,以确保所有单独的类都正确地相互集成:)

      【讨论】:

      • 为了提供更多背景知识,Bar 实际上负责对消息进行排序,并且根据现有协议,初始序列号是可配置的。 Foo.DoSomething() 实际上将传递消息和序列号,Bar 在处理之前使用它们对消息进行排序。
      • 但是为什么 Foo 需要知道初始数字呢?
      • Grzenio,Foo 不需要知道它,除了我在 Foo 内部用它构建了 Bar 的事实。我可以将它传递给构造函数之外的 Bar ,但是已经采用了另一个答案中建议的抽象工厂模式方法。谢谢。
      【解决方案5】:

      我只是重载构造函数,有一个接受 x,另一个接受 Bar。

      这使您的对象更易于使用和测试。

      //containing class omitted
      public Foo(Bar bar) {
          this.bar = bar;
      }
      
      public Foo(int x) {
          this(new DefaultBar(x));
      }
      

      这就是构造函数重载的目的。

      【讨论】:

      • 我看不出这些构造函数中的任何一个如何让我测试 Foo 正确初始化 Bar。前者 Foo 没有初始化 Bar,后者 Bar 不能被嘲笑。
      猜你喜欢
      • 2016-07-13
      • 2017-12-13
      • 1970-01-01
      • 2021-08-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-10
      相关资源
      最近更新 更多