Статьи

Рефакторинг, чтобы разрешить двойники теста

Иногда, когда вы создаете класс, он напрямую создает экземпляр объекта для использования в своих методах. Например:

1
2
3
4
5
6
public void myFunc()
{
   MyType object = new MyType();
   object.doSomething();
   this.thingy = object.getSomething();
}

Обычно это считается плохим, поскольку вы тесно связываете свой класс с экземпляром. «Но, — говорите вы, — я знаю, что никогда не буду использовать какой-либо другой класс, и я, конечно же, не хочу, чтобы пользователи этого класса предоставляли альтернативные варианты использования». Эти проблемы в основном справедливы, но они упускают очень важную вещь, на которую следует обратить внимание: тестирование.

Вы должны быть в состоянии заменить этот объект тестовым двойником (часто ошибочно называемым mocks; mocks — это особый тип double ) по всем обычным причинам наличия двойников. Но, как есть, вы не можете заменить его. Это позволило бы всем пользователям моего класса сделать это тоже, не так ли?

Что если я скажу вам, что вы можете съесть свой торт и съесть его тоже? (такая странная фраза …) Ну, вы можете, и я дам вам краткое объяснение о том, как, а затем перейти к деталям.

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

Ореховая скорлупа

Предполагая, что вы выполняете свои тесты типичным способом их размещения в одном пакете с тем, что они тестируют, но в другом каталоге, вы можете очень легко настроить что-то, чтобы заменить ваш «неизменяемый» объект на двойной тест. Все, что вам нужно, это package-private (доступ по умолчанию), чтобы предоставить классу двойной тип и private способ доступа к нему. Затем вы можете использовать package-private вызовы в своих тестах, чтобы предоставить классу двойной тест.

Есть несколько способов сделать каждую часть, так что я углублюсь в это.

Поставщики

Я могу придумать три различных способа сказать объекту тестирования, какой тип двойного использовать, и оказалось, что это в точности три основных способа выполнения типичного внедрения зависимостей:

  1. Конструктор
  2. Сеттер (он же Мутатор)
  3. поле

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

Конструктор Инъекция

Создайте в своем классе конструктор package-private который принимает нормальные параметры плюс тест double. Тогда ваши существующие конструктор (ы) и / или статический метод (ы) фабрики могут делегировать этому конструктору объект по умолчанию.

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

Использование конструктора также уменьшает количество строк, требуемых в ваших тестах, так как это строка, которую вы уже собираетесь печатать.

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

Сеттер Инъекция

Создайте метод package-private установщик package-private который может вызвать тест, чтобы установить поле для двойника теста. Конструктор устанавливает для поля значение по умолчанию или ноль (метод для доступа к тесту double может проверить ноль и использовать значение по умолчанию, если оно есть).

Это не ужасный вариант, но, как указано выше, определенно не идеален для неизменных классов. Лучшая часть этого — его очевидность. Видение вызываемого сеттера проясняет, что делает там тестовый двойник, но обычно не намного яснее. Другим преимуществом, как говорится в предыдущем разделе, является возможность изменения удвоения без создания нового экземпляра этого класса. Опять же, это не так уж и полезно, правда.

Полевая инъекция

Это делается путем создания поля, в котором хранится двойной package-private и непосредственного назначения ему двойного package-private .

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

Технически, это вариант, который работает, но это единственная причина, по которой я даже потрудился включить его в статью. Это плохой дизайн; избежать этого любой ценой. Пожалуйста.

Accessors

Средства доступа почти всегда private . Обычно это происходит потому, что мы даже не хотели их получать, пока не начали рефакторинг нашего кода, чтобы использовать двойные тесты. Я могу думать только о двух типах аксессуаров, которые являются зеркалами поставщиков. Поскольку вы не можете сделать зеркало конструктора, есть только два:

  1. добытчик
  2. поле

Опять же, оба находятся в предпочтительном порядке, хотя разница между тем, насколько я предпочитаю одно над другим, значительно меньше.

Получатель доступа

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

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

Доступ к полю

Просто используйте private поле, когда вам нужно использовать double или значение по умолчанию. Это действительно просто.

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

Обертывание от перегрузки или «скрытый вызов»

Этот метод игнорирует идею использования package-private поставщиков package-private и private доступа и работает совершенно по-другому, даже не требуя добавления поля к классу. Это особенно полезно, если только один метод использует объект, который нуждается в двойном объекте (хотя эта ситуация может указывать на то, что ваш класс нарушает SRP).

Техника включает в себя перегрузку метода, который вы хотите протестировать, с помощью метода package-private который принимает зависимость в качестве параметра. Затем вы перемещаете всю логику из оригинала в перегрузку, заменяя заблокированную зависимость зависимостью, переданной параметром. Затем оригинальный метод просто вызывает новую перегрузку и передает исходный объект. Например, вы можете включить это

1
2
3
4
5
6
public void myFunc()
{
   MyType object = new MyType();
   object.doSomething();
   this.thingy = object.getSomething();
}

в это

01
02
03
04
05
06
07
08
09
10
11
public void myFunc()
{
   MyType object = new MyType();
   myFunc(object);
}
 
void myFunc(MyType object)
{
   object.doSomething();
   this.thingy = object.getSomething();
}

Теперь вы можете пройти двойной тест и изолировать функциональность myFunc() .

Экземпляры и фабрики

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

Если вы еще не читали его, я недавно написал пост о создании простых функциональных фабрик на Java и Python .

Проверьте значения по умолчанию

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

Разочаровывающий побочный эффект

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

Это полностью зависит от вас; Я просто предоставил заявление об отказе (и подсказку :)).

Потрясающая побочная выгода?

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

Фактически, вы можете взять всю эту статью и заменить каждый экземпляр « package-private » на « public », чтобы сделать его общей статьей о предоставлении внедрения зависимостей.