Как инъекция зависимостей способствует тестируемости - PullRequest
6 голосов
/ 20 декабря 2011

Я читал шаблон Factory и наткнулся на статьи, в которых предлагается использовать шаблон Factory в сочетании с внедрением зависимостей для максимизации возможности повторного использования и тестирования. Хотя я не смог найти никаких конкретных примеров этого гибрида Factory-DI, я собираюсь попробовать и привести несколько примеров кода моей интерпретации . Однако мой вопрос действительно о том, как этот подход улучшает тестируемость.

Моя интерпретация:

Итак, у нас есть Widget класс:

public class Widget {
    // blah
}

И мы хотим включить WidgetFactory для управления построением Widget s:

public interface WidgetFactory {

    public abstract static Widget getWidget();
}

public class StandardWidgetFactory implements WidgetFactory {

    @Override
    public final static Widget getWidget() {
        // Creates normal Widgets
    }
}

public class TestWidgetFactory implements WidgetFactory {

    @Override
    public final static Widget getWidget() {
        // Creates test/mock Widgets for unit testing purposes
    }
}

Хотя в этом примере используется Spring DI (это единственный API, с которым я сталкивался), на самом деле не имеет значения, говорим ли мы о Guice или какой-либо другой инфраструктуре IoC; Идея в том, что теперь мы собираемся внедрить правильную WidgetFactory реализацию во время выполнения, чтобы зависеть от того, тестируем ли мы код или работаем нормально. Весной конфигурация bean-компонентов может выглядеть следующим образом:

<bean id="widget-factory" class="org.me.myproject.StandardWidgetFactory"/>
<bean id="test-widget-factory" class="org.me.myproject.TestWidgetFactory"/>

<bean id="injected-factory" ref="${valueWillBeStdOrTestDependingOnEnvProp}"/>

Затем в коде:

WidgetFactory wf = applicationContext.getBean("injected-factory");
Widget w = wf.getWidget();

Таким образом, переменная среды (уровня развертывания), возможно, определенная где-то в файле .properties , решает, будет ли Spring DI внедрять StandardWidgetFactory или TestWidgetFactory.

Правильно ли я делаю это?!? Кажется, что очень много инфраструктуры просто получают хорошую тестируемость для моего Widget. Не то чтобы я был против этого, но мне кажется, что это слишком изощренно.

Мое зависание:

Причина, по которой я спрашиваю это , заключается в том, что у меня будут другие объекты в других пакетах, в которых есть методы, использующие Widget объекты внутри них. Возможно что-то вроде:

public class Fizz {
    public void doSomething() {

        WidgetFactory wf = applicationContext.getBean("injected-factory");
        Widget widget = wf.getWidget();

        int foo = widget.calculatePremable(this.rippleFactor);

        doSomethingElse(foo);
    }
}

Без этой огромной, по-видимому, излишне сложной установки, я бы не смог внедрить «mock Widgets» в мой модульный тест для Fizz::doSomething().

Так что я разорван: с одной стороны мне просто кажется, что я слишком много думаю - что я вполне могу делать (если моя интерпретация неверна ). С другой стороны, я не вижу чистого способа обойти это.

В качестве продолжения касательного вопроса это также вызывает еще одну мою огромную озабоченность: если моя интерпретация верна (или даже несколько верна), то означает ли это, что нам нужно Factories для каждого объекта?!?

Звучит, как путь-переобучение ! Что за отсечка? Какая линия на песке разграничивает использование фабрики, а когда нет ?! Спасибо за любую помощь и извинения за подробный вопрос. Просто у меня кружится голова.

Ответы [ 6 ]

6 голосов
/ 20 декабря 2011

DI / IoC помогает тестировать, потому что вы можете легко решить, какую реализацию использовать, не изменяя код, который ее использует. Это означает, что вы можете внедрить известную реализацию для реализации определенных функций, например, имитировать сбой веб-службы, гарантировать хороший (или плохой) ввод для функции и т. Д.

Фабрики не обязаны обеспечивать работу DI / IoC. Требуется ли фабрика, зависит полностью от особенностей использования.

public class Fizz {

    @Inject    // Guice, new JEE, etc. or
    @Autowired // Spring, or
    private Widget widget;

    public void doSomething() {
        int foo = widget.calculatePremable(this.rippleFactor);
        doSomethingElse(foo);
    }

}
2 голосов
/ 20 декабря 2011

Я думаю, что ответ на этот вопрос, по сути, прост: преимущества Inpendency Injection могут быть реализованы с использованием обычного Java, но за счет добавления большого количества котельной плиты.

DI позволяет вам отделять и модульно выполнять ваши классы, не добавляя при этом кучу параметризованных конструкторов / методов Factory и т. Д. К базе исходного кода.

2 голосов
/ 20 декабря 2011

В вашем примере с классом Fizz, это анти-паттерн DI, который делает явный вызов в DI-фреймворк для получения нужного компонента. Весь смысл DI - принцип Голливуда (не звоните нам, мы вам позвоним). Таким образом, ваша структура DI должна внедрять виджет в Fizz.

Когда дело доходит до тестирования, я предпочитаю

  • тестовое устройство создает заглушку или макет зависимости. Или реальная вещь, когда это уместно.
  • тестовое устройство вводит заглушку, используя тот же метод конструктора или метода установки, который в производстве будет использоваться моей структурой DI.

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

Вывод из всего этого заключается в том, что я не вижу особой пользы для шаблона Фабрики.

Можете ли вы дать нам ссылку на статьи, которые вы читали? Я посмотрю, смогу ли я выкопать один или два сам.

Редактировать: Вот обещанная ссылка: Является ли внедрение зависимостей заменой фабричных шаблонов? , одна из серии довольно хороших статей о DI.

0 голосов
/ 20 декабря 2011

DI позволяет программировать интерфейсы без использования фабрики.Программирование для интерфейсов и возможность передачи в конкретной форме реализации вне класса - это то, что облегчает тестирование кода.

interface Widget {
}

class UserOfWidget {
   Widget widget;
   public UserOfWidget(Widget widget) {
      this.widget = widget
   }
}

Using spring dependency injection
<bean id="widget" class="ConcreteWidget"/>
<bean class="UserOfWidget">
   <constructor-arg ref="widget"/>
</bean>

In a test case
UserOfWidget uow = new UserOfWidget(new StubWidget()); 

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

Тем не менее, есть основания использовать фабрики, даже когда вы используете DI.В основном для сценариев, где построение объекта является сложным - требует нескольких шагов, не может быть сделано простым вызовом конструктора.Одним из таких примеров является получение объекта из JNDI (источник данных, например).Вся логика поиска объекта из JNDI инкапсулирована на фабрике - которую затем можно настроить как простой компонент в файле XML.

Подводя итог - вам не нужны фабрики с DI, чтобы сделать код болеепроверяемый.Просто DI достаточно.Заводы требуются только для упрощения создания объектов, которые сложно создавать.

0 голосов
/ 20 декабря 2011

Внедрение зависимостей определенно делает код более тестируемым. Когда я прихожу к непроверенному EJB3-коду, который использует внедрение Dependency, это похоже на день и ночь по сравнению с проверкой других типов устаревшего кода.

Что касается заводов, они могут иметь две роли. Одним из них является внедрение зависимостей, когда у вас нет структуры внедрения зависимостей. Это статический шаблон фабрики, о котором говорится в эффективной Java , который на самом деле довольно уникален для Java и лучше всего сделан по другим причинам (подробно описано там). Некоторые из них вы видите в JDK, и это медвежье тестирование, и, конечно же, оно не делает тестируемый код естественным образом, поскольку вы должны быть очень осторожны в его использовании.

Еще одна роль для фабрик, где вы используете истинный шаблон «Банды четырех», в внедрении зависимостей - это то, где конструирование реального объекта несколько связано, а не простой конструктор. В этом случае вы вводите фабрику, которую можно представить в простом интерфейсе, а затем выполняете фабричную реализацию. Это больше построено вокруг принципа программирования, нет проблемы, которая не может быть решена с помощью еще одного уровня косвенности.

На самом деле, только когда я начал делать инъекцию зависимостей Guice, я обнаружил потребность в фабричном шаблоне GOF в Java.

0 голосов
/ 20 декабря 2011

У вас есть фабрика, но вы не используете Dependency Injection в своем классе Fizz. Вот почему вам нелегко его протестировать. Вам нужно ввести либо applicationContext, либо WidgetFactory напрямую. Я предпочитаю последнее в большинстве случаев.

public class Fizz {
    private WidgetFactory wf;
    public Fizz(WidgetFactory widgetFactory) {
        wf = widgetFactory;
    }

    public void doSomething() {

        Widget widget = wf.getWidget();

        int foo = widget.calculatePremable(this.rippleFactor);

        doSomethingElse(foo);
    }
}

Теперь, когда у вас есть это, вы можете добавить фиктивные объекты в ваш класс Fizz. В приведенном ниже примере я использую Mockito, но вы можете использовать другие фальшивые фреймворки (или создать свой собственный макет, если вы действительно хотите).

@Test
public void test() {
    WidgetFactory wf = mock(WidgetFactory.class); // mock is a Mockito function that creates mocks for you automatically.
    Fizz objectUnderTest = new Fizz(wf);

    // Test Fizz
}

В вашем модульном тестировании вы действительно не хотите загружать applicationContext. Вот почему вы издеваетесь над ним в коде, а затем передаете его напрямую. Таким образом, у вас будет только одно простое applicationContext и это для производства.

<bean id="widget-factory" class="org.me.myproject.StandardWidgetFactory"/>
<bean id="fizz" class="org.me.myproject.Fizz">
    <constructor-arg ref="widget-factory"/>
</bean>

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

...