Введение

Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse.

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

When a patient feels a little better, often that is the time when you can help him make commitments to a healthier life style. That is what we are shooting for with legacy code.

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

Часть 1. Механика изменений

Глава 1. Изменяя ПО

Behavior is the most important thing about software. It is what users depend on. Users like it when we add behavior (provided it is what they really wanted), but if we change or remove behavior they depend on (introduce bugs), they stop trusting us.

Роберт Мартин ( #МартинРоберт ) придерживается другой точки зрения, считая самой важной ценностью ПО не поведение, а структуру: Две ценности ПО - поведение и структура.


It seems nearly impossible to add behavior without changing it to some degree.

Исходим из следующих моментов:

  1. Если мы модифицируем существующий код - то мы с огромной вероятностью изменяем поведение.
  2. Если мы только добавляем код (и организуем его выполнение - это важно) - то мы добавляем новое поведение. Open-Closed Principle - OCP именно об этом.
  3. Организация вызова новых методов - это как правило изменение существующего кода.

@code-change-reasons ^code-change-reasons

Автор выделил четыре причины внесения изменений в код:

  • добавление фичи,
  • исправление ошибок,
  • рефакторинг
  • оптимизация

Все они должны сохранять большую часть существующей функциональности (поведения) нетронутой.

Preserving existing behavior is one of the largest challenges in software development. Even when we are changing primary features, we often have very large areas of behavior that we have to preserve.

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


@avoiding-changes ^avoiding-changes

Избегание изменений - это консервативная и неэффективная стратегия борьбы с риском при внесении изменений в программный код. Негативные последствия:

  1. Боязнь изменений, так как нет уверенности в неизменности поведения и изменения не вошли в привычку.
  2. Укрупнение классов и методов, ведущее к усложнению системы в целом.

Глава 2. Работа с обратной связью

Обычно тестирование осуществляется после окончания разработки. Такое тестирование призвано подтвердить корректную работоспособность продукта, однако по сути не существует набора тестов, которые бы гарантировали отсутствие дефектов (см Научные теории недоказуемы, но опровергаемы). Более того, такое тестирование замедляет получение разработчиками обратной связи, т.к. оторвано по времени от стадии разработки.

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


Назначение модульных тестов.


Дилемма унаследованного кода

Для изменения кода мы должны иметь страховочные тесты. Для создания таких тестов мы должны изменить код.

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


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

Алгоритм внесения изменений в унаследованный код

  1. Определить точки изменений. Главы 16 и 17
  2. Найти точки тестирования. Главы 11 и 12
  3. Разорвать зависимости. Главы 7, 9, 10, 22 и 23
  4. Написать тесты. Глава 13
  5. Внести требуемые изменения и провести рефакторинг. Главы 8, 20, 21 и 22

Глава 3. Чувствовать и разделять

В идеале нам не нужно как-то особенно готовить каждый класс к тестированию. Суть в том, что идеальный класс не должен тащить за собой вагон зависимостей (любого вида, в том числе и транзитивные @Autowired), и тогда его помещение в тестовую среду будет очень простым делом.

При подготовке тестовой среды зависимости между классами необходимо разорвать по двум причинам:

  1. Почувствовать. Мы разрываем зависимости, чтобы увидеть значения, рассчитанные внутри нашего кода. Например такие, которые “готовятся” внутри метода, нигде не сохраняются, а просто передаются в какой-то другой класс.
  2. Разделить. Мы разрываем зависимости, чтобы получить возможность поместить класс в тестовую среду.

Поддельные реализации

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

Автор считает, что простых “самописных” поддельных объектов должно быть достаточно для большинства ситуаций тестирования.

Глава 4. Модель шва

Переиспользование компонентов - непростая задача. Насколько часто маленькие части программ действительно переиспользуются? Не очень часто. Даже когда отдельные фрагменты выглядят независимыми, в действительности они могут зависеть друг от друга совершенно неочевидными способами.

Шов - это место, в котором мы можем изменить поведение программы, не меняя ее код в этом самом месте.

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

Точка активации - место, где выбирается конкретная реализация шва.

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

Глава 5. Инструменты

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


UI - это не лучшее место для тестов. Интерфейсы обычно очень изменчивы и слишком далеки от тестируемой функциональности. Когда падают UI-тесты, сложно сразу найти причину. Несмотря на это команды тратят значительные ресурсы, пытаясь организовать все тестирование через UI.

Часть 2. Внесение изменений в ПО

Глава 6. У меня мало времени, но я должен внести изменения

Покрытие кода тестами - это дополнительная работа, которая может казаться совершенно излишней прямо сейчас, но она окупится в будущем. Когда? Зависит от проекта :)

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

Алгоритм работы должен быть примерно таков:

  1. Попробуйте получить экземпляр класса в изолированной тестовой среде. Если это получилось - напишите тесты.
  2. Если создать экземпляр не получилось, обратитесь к главам 9 " Я не могу создать объект в тестовой среде " и 10 " Я не могу выполнить этот метод в тестовой среде “. Выполните рекомендации из этих глав.
  3. Если времени на разрыв зависимостей недостаточно, или это кажется опасной операцией, проанализируйте требуемые изменения. Можно ли выразить их в виде абсолютно нового, независимого от существующего, кода? Во многих случаях это вполне возможно.
  4. Если изменения можно представить в виде нового кода, воспользуйтесь одной из техник этой главы: Отведение метода, Отведение класса, Обертка метода, Обертка класса. Методы добавления нового кода