Постепенные подходы к внедрению зависимости

12

Я работаю над тем, чтобы сделать мои классы модульно-тестируемыми, используя внедрение зависимостей. Но некоторые из этих классов имеют много клиентов, и я не готов реорганизовать их все, чтобы начать передавать зависимости. Поэтому я пытаюсь сделать это постепенно; пока сохраняем зависимости по умолчанию, но позволяем переопределять их для тестирования.

Один из подходов, который я придумаю, - это просто перевести все «новые» вызовы в их собственные методы, например:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Затем в своих модульных тестах я могу просто создать подкласс этого класса и переопределить функции create, чтобы они вместо этого создавали поддельные объекты.

Это хороший подход? Есть ли недостатки?

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

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

JW01
источник
Можете ли вы уточнить, почему они сейчас не тестируются на модуле?
Остин Салонен
В коде разбросано множество «новых X», а также множество статических классов и синглетонов. Поэтому, когда я пытаюсь протестировать один класс, я действительно тестирую целую кучу их. Если я смогу изолировать зависимости и заменить их поддельными объектами, у меня будет больше контроля над тестированием.
JW01

Ответы:

13

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

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

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

Петер Тёрёк
источник
Просто замечание: «Как только у вас есть тесты, вы можете проводить рефакторинг более свободно» - если у вас нет тестов, вы не рефакторингу, вы просто меняете код.
ocodo
@Slomojo Я понимаю вашу точку зрения, но вы все еще можете сделать много безопасного рефакторинга без тестов, в частности, автоматических инструкций IDE, таких как, например, (в eclipse для Java) извлечение дублированного кода в методы, переименование всего, перемещение классов и извлечение полей, констант и т.д.
Alb
Я просто цитирую происхождение термина Refactoring, который модифицирует код в улучшенные шаблоны, защищенные от ошибок регрессии средой тестирования. Конечно, рефакторинг на основе IDE сделал его более простым для «рефакторинга» программного обеспечения, но я предпочитаю избегать самообмана, если мое программное обеспечение не имеет тестов, а я «рефакторинг», я просто изменяю код. Конечно, это может быть не то, что я говорю кому-то еще;)
ocodo
1
@Alb, рефакторинг без тестов всегда более рискован. Автоматизированный рефакторинг, безусловно, снижает этот риск, но не до нуля. Всегда есть сложные крайние случаи, с которыми инструменты могут не справиться правильно. В лучшем случае результатом является какое-то сообщение об ошибке от инструмента, но в худшем случае оно может незаметно внести незначительные ошибки. Поэтому рекомендуется придерживаться максимально простых и безопасных изменений даже с помощью автоматизированных инструментов.
Петер Тёрёк
3

Guice-люди рекомендуют использовать

@Inject
public void setX(.... X x) {
   this.x = x;
}

для всех свойств вместо простого добавления @Inject к их определению. Это позволит вам рассматривать их как обычные bean-компоненты, которые вы можете использовать newпри тестировании (и настройке свойств вручную) и @Inject в производственной среде.


источник
3

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

Итак, мой новый класс для тестирования модулей будет иметь 2 конструктора:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}
Сет М.
источник
2

Возможно, вы захотите рассмотреть идиому «геттер создает», пока не сможете использовать внедренную зависимость.

Например,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Это позволит вам выполнить внутренний рефакторинг, чтобы вы могли ссылаться на свой myObject через геттер. Вам никогда не придется звонить new. В будущем, когда все клиенты будут настроены для разрешения внедрения зависимостей, все, что вам нужно сделать, это удалить код создания в одном месте - получателе. Сеттер позволит вам вводить фиктивные объекты по мере необходимости.

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

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

Гэри Роу
источник
Благодарю. Я рассматриваю этот подход для некоторых случаев. Но что вы делаете, когда конструктору MyObject требуются параметры, которые доступны только внутри вашего класса? Или когда вам нужно создать более одного MyObject в течение жизни вашего класса? Нужно ли вводить MyObjectFactory?
JW01
@ JW01 Вы можете настроить код создателя-получателя так, чтобы он читал внутреннее состояние вашего класса и просто вписывал его. Создание более одного экземпляра в течение жизни будет сложно организовать. Если вы столкнетесь с такой ситуацией, я бы предложил редизайн, а не обходной путь. Не делайте ваши добытчики слишком сложными, иначе они станут жерновами на вашей шее. Ваша цель - облегчить процесс рефакторинга. Если это кажется слишком сложным, переместитесь в другую область, чтобы исправить это там.
Гэри Роу
это синглтон. Только не используйте синглтоны O_O
CoffeDeveloper
@DarioOO На самом деле это очень плохой синглтон. Лучшая реализация использовала бы перечисление согласно Джошу Блоху. Синглтоны - это действительный шаблон дизайна, который можно использовать (дорогостоящее одноразовое создание и т. Д.)
Гэри Роу