Модульное тестирование строительства объекта / инициализация - PullRequest
1 голос
/ 28 июня 2010

У меня есть класс Foo, который использует другой класс Bar для некоторых вещей.

Я пытаюсь выполнить тестовую разработку, и поэтому пишу модульные тесты для Foo, чтобы убедиться, что он вызывает соответствующие методы для Bar, и для этой цели я использую внедрение зависимостей и Mocking 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 труднее в использовании.

Я подозреваю, что может быть какое-то решение, которое включало бы использование контейнера IoC, настроенного для внедрения Foo с фиктивным IBar, а также для предоставления доступа к созданному макету для проверки ожидаемого, но я чувствую, что это сделает Foo излишне сложным. Я использую только внедрение зависимостей, чтобы позволить мне смоделировать зависимости для тестирования, и поэтому в настоящее время я не использую контейнеры IoC, а просто строю зависимости в цепочечном вызове конструктора из конструкторов по умолчанию (хотя я понимаю, что это создает более непроверенный код) например,

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

Кто-нибудь может порекомендовать лучший способ проверки построения / инициализации зависимого объекта?

Ответы [ 5 ]

2 голосов
/ 28 июня 2010

Мне кажется, что вы позволяете деталям реализации просачиваться через дизайн API.IBar не должен ничего знать о Foo и его требованиях.

Реализация панели с Вводом конструктора , как вы это делали с 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 без пропуска деталей реализации.Отделение IBar от x можно рассматривать как реализацию принципа разделения интерфейса .

В терминологии DI-контейнера x можно рассматривать как Singleton-ограничен (не путать с шаблоном проектирования Singleton), поскольку существует только один экземпляр x во многих различных компонентах.Экземпляр shared , и он строго относится к управлению жизненным циклом - не относится к разработке API.

Большинство DI-контейнеров поддерживают определение службы как Singleton,но DI-контейнер не требуется.Как показывает приведенный выше пример, вы также можете подключить общие службы вручную.

В тех случаях, когда вы не знаете значение x до времени выполнения, Абстрактная фабрика является универсальнойрешение .

1 голос
/ 28 июня 2010

Определите эту переменную x в вашей структуре внедрения зависимостей и вставьте ее в Foo и Bar.

0 голосов
/ 28 июня 2010

Я бы просто перегрузил конструктор, чтобы один принимал x, а другой для бара.

Это облегчает использование и тестирование вашего объекта.

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

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

Это то, для чего была изобретена перегрузка конструктора.

0 голосов
/ 28 июня 2010

Это больше вопрос дизайна.Зачем Foo инициализация Bar с определенным значением x?Потенциально границы классов не определены оптимально.Может быть, 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();
  }
}

Этот пример также показывает, что вам нужно провести несколько интеграционных тестов, чтобы убедиться, что все отдельные классы правильно интегрированы друг с другом:)

0 голосов
/ 28 июня 2010

Как создается Foo? Это происходит из-за инверсии контрольного контейнера? Если это так, вы можете использовать функции в контейнере IoC для настройки этих объектов.

Если нет, просто прикусить пулю и добавить метод Foo.Initializer (параметр), который вызывает Bar.Initialize (параметр) или, возможно, принимает экземпляр Bar.

Я не занимался модульным тестированием намного дольше, чем вы, но я обнаружил, что больше ничего не делаю в конструкторах. Это просто мешает тестированию. Конструктор берет зависимости объектов, затем его готово.

...