2010-06-28 4 views
1

У меня есть класс Foo, который использует другой класс Bar, чтобы сделать что-то.Конструкция/инициализация объекта тестирования объекта

Я пытаюсь выполнить тестовую разработку, и поэтому я пишу модульные тесты для Foo, чтобы убедиться, что он называет соответствующие методы в Bar, и для этой цели я использую инъекцию зависимостей и издевательскую Bar (используя Rhino Mocks).

E.g. (В 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); 
} 

Я чувствую, что это делает Бар более сложным в использовании.

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

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

Может ли кто-нибудь рекомендовать лучший способ проверить конструкцию/инициализацию зависимых объектов?

ответ

2

Мне кажется, что вы даете детализации реализации через дизайн 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 можно рассматривать как реализацию Interface Segregation Principle.

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

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

В тех случаях, когда вы не знаете значения x до времени исполнения, Abstract Factory is the universal solution.

+0

Foo не заботится о x, за исключением того факта, что Bar должен быть инициализирован им. Я уточнил свой вопрос, чтобы прояснить это. Благодарю. – Ergwun

+0

@Ergwun: Если Foo не заботится о x, просто создайте Bar с x и введите его в Foo. Если x не может быть известно до времени выполнения, используйте абстрактную фабрику для создания IBar в Foo - см. Последнюю ссылку моего ответа. –

+0

Попытавшись это на некоторое время, я нашел это использование фабрики абстрактных рисунков, чтобы быть чрезвычайно полезным для этой проблемы. Благодаря! – Ergwun

1

Определите эту переменную x в своей инфраструктуре инъекции зависимостей и введите ее как в Foo, так и в Bar.

0

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

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

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

+0

В настоящее время Foo не приходит из контейнера IoC. Отдельное решение для инициализаций - это тот, на который я сейчас склоняюсь. Другим недостатком этого было бы замалчивать все другие общедоступные методы утверждениями и/или исключениями для защиты от использования неинициализированных объектов, если это не потребовалось бы, если конструктор мог выполнить инициализацию. Благодарю. – Ergwun

0

Это больше вопрос о дизайне. Почему Foo нужен 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

Чтобы дать немного больше фона, Bar фактически отвечает за последовательность сообщений, и согласно существующему протоколу, начальный порядковый номер настраивается. Foo.DoSomething() будет фактически передавать сообщения и порядковые номера, которые Бар использует для заказа сообщений перед обработкой. – Ergwun

+0

Но почему Foo нужно знать об исходном номере? – Grzenio

+0

Grzenio, Foo не нужно это знать, за исключением того факта, что у меня был Bar, построенный с ним в недрах Foo. Я мог бы передать его в Bar вне конструктора, но сменил абстрактный подход к шаблону фабрики, предложенный в другом ответе. Благодарю. – Ergwun

0

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

Это делает ваш объект более простым в использовании и проще в тестировании.

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

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

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

+0

Я не вижу, как любой из этих конструкторов делает это для меня, чтобы проверить, правильно ли Foo инициализирует Bar. В бывшем Foo не инициализируется Bar, а в последнем баре нельзя издеваться. – Ergwun

Смежные вопросы