简体   繁体   English

单元测试对象的构造/初始化

[英]Unit testing object construction/initialization

I have a class Foo that uses another class Bar to do some stuff. 我有一个Foo类,它使用另一个Bar类来做一些事情。

I'm trying to do test driven development, and therefore am writing unit tests for Foo to make sure it calls the appropriate methods on Bar, and for this purpose I'm using dependency injection and mocking Bar (using Rhino Mocks). 我正在尝试进行测试驱动的开发,因此正在为Foo编写单元测试,以确保它在Bar上调用了适当的方法,为此,我正在使用依赖注入和模拟Bar(使用Rhino Mocks)。

Eg (in C#): 例如(在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();
  }
}

This all seems to be fine, but I actually want Bar to be configured with a particular parameter, and was going to have this parameter passed in via Foo's constructor. 一切似乎都很好,但是我实际上希望为Bar配置一个特定的参数,并打算通过Foo的构造函数传递此参数。 Eg (in C#): 例如(在C#中):

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

I'm not sure which is the best way to change this to be more easily testable. 我不确定哪一种是最好的更改方式,使其更易于测试。 One options I can think of involves moving the initialization of Bar out of its constructor, eg 我能想到的一个选择涉及将Bar的初始化移出其构造函数,例如

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

I feel that this is making Bar harder to use though. 我觉得这使Bar难以使用。

I suspect there may be some kind of solution that would involve using an IoC container configured to inject Foo with a mock IBar and also provide access to the created mock for expectation validation, but I feel this would be making Foo unnecessarily complex. 我怀疑可能存在某种解决方案,其中涉及使用配置为使用模拟IBar注入Foo的IoC容器,并提供对创建的模拟的访问以进行期望验证,但是我认为这会使Foo变得不必要地复杂。 I'm only using dependency injection to allow me to mock the dependencies for testing, and so am not using IoC containers at present, just constructing dependencies in a chained constructor call from the default constructors (although I realize this is creating more untested code) eg 我仅使用依赖项注入来模拟依赖项以进行测试,因此目前不使用IoC容器,而只是通过默认构造函数在链式构造函数调用中构建依赖项(尽管我意识到这正在创建更多未经测试的代码)例如

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

Can anyone recommend the best way to test dependent object construction/initialization? 谁能推荐测试依赖对象构造/初始化的最佳方法?

It seems to me that you are letting an implementation detail leak through the API design. 在我看来,您正在让实现细节通过 API设计泄漏 IBar shouldn't know anything about Foo and its requirements. IBar对Foo及其要求一无所知。

Implement Bar with Constructor Injection just like you did with Foo. 就像使用Foo一样,使用构造函数注入实现Bar。 You can now make them share the same instance by supplying it from the outside: 现在,您可以通过从外部提供它们来使它们共享同一实例:

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

This would require your Foo constructor to look like this: 这将需要您的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;
    }
}

This allows Foo to deal with any implementation of IBar without letting the the implementation details leak through. 这使Foo可以处理IBar的任何实现,而不会泄漏实现细节。 Separating IBar from x can be viewed as an implementation of the Interface Segregation Principle . IBarx IBar可以看作是接口隔离原理的一种实现。

In DI Container terminology, x can be viewed as being Singleton-scoped (not to be confused with the Singleton design pattern) because there is only a single instance of x across many different components. 在DI容器术语中, x可以看作是Singleton范围的 (不要与Singleton设计模式相混淆),因为在许多不同的组件中只有x的单个实例。 The instance is shared and it is strictly a lifetime management concern - not an API design concern. 实例是共享的严格来说是生命周期管理问题,而不是API设计问题。

Most DI Containers have support for defining a service as a Singleton, but a DI Container is not required. 大多数DI容器都支持将服务定义为Singleton,但是不需要DI容器。 As the above example shows, you can also wire shared services up by hand. 如以上示例所示,您还可以手动连接共享服务。

In the cases where you don't know the value of x until run-time, Abstract Factory is the universal solution . 在您直到运行时才知道x值的情况下, Abstract Factory是通用解决方案

在依赖项注入框架中定义此变量x,并将其注入Foo和Bar。

How is Foo created? Foo是如何创建的? Is it coming from an inversion of control container? 它来自控制容器的反转吗? If so you may be able to use features in your IoC container to configure these objects. 如果是这样,您也许可以使用IoC容器中的功能来配置这些对象。

If not, just bite the bullet and add a method Foo.Initializer(parameter) which calls Bar.Initialize(parameter) or perhaps takes an instance of Bar. 如果不是,只需咬一下子弹,然后添加方法Foo.Initializer(parameter),该方法调用Bar.Initialize(parameter)或采用Bar的实例。

I haven't been doing unit testing that much longer then you, but I've found I don't do things in constructors anymore. 我没有比您做单元测试更长的时间,但是我发现我不再在构造函数中做任何事情。 It just gets in the way of testing. 它只是妨碍测试。 A constructor takes the objects dependencies, then its done. 构造函数接受对象的依赖关系,然后完成。

This is more a question about design. 这更多是关于设计的问题。 Why does Foo need Bar initialized with a particular value of x? Foo为什么需要用特定的x值初始化Bar? Potentially the class boundaries are not defined optimally. 可能没有最佳定义类边界。 Maybe Bar shouldn't encapsulate the value of x at all, just take it as a parameter in the method call: 也许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);
  }
}

If for some reason you can't change the design I think I would just have an assertion in the constructor: 如果由于某种原因您不能更改设计,我想我只会在构造函数中有一个断言:

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();
  }
}

This example also shows that you need to have some integration tests to make sure all the individual classes are correctly integrated with each other :) 这个例子还表明,您需要进行一些集成测试,以确保所有单个类都正确地相互集成:)

I'd just overload the constructor, have one that accepts x, another that accepts a Bar. 我只是重载了构造函数,有一个接受x,另一个接受Bar。

This makes your object easier to use and easier to test. 这使您的对象更易于使用和测试。

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

public Foo(int x) {
    this(new DefaultBar(x));
}

This is what constructor overloading was invented for. 这就是构造函数重载的目的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM