Тема юнит-тестирования не устает подниматься в блогах и вебинарах. Раз тема актуальна, выскажу и я некоторые свои мысли.

У всех подобных статей и вебинаров есть одна общая и очень серьезная проблема. Людей убеждают, что тестирование — это хорошо, показывают на простых примерах как использовать инструменты. Многим это действительно интересно, люди слушают, загораются энтузиазмом. А на следующий день приходят на работу, видят свой старый проблемный код (а у кого он не проблемный?) и понимают, что тестирование вероятно не для них. Приходится им приглушить свой энтузиазм до следующего раза.

Для всех этих людей проповедники юнит-тестирования предлагают один и тот же рецепт: «читайте книжки про рефакторинг, исправляйте архитектуру». Хороший совет. Примерно такой же хороший как «пишите хорошие программы» или «мойте руки перед едой».

Я позволил себе немного иронии, но совет действительно хороший. Практика многих людей показывает, что разработка с учетом необходимости тестирования кода положительно влияет на стройность архитектуры. Иногда даже доходит до крайностей (извини, Александр):

ПРАВИЛЬНУЮ АРХИТЕКТУРУ без тестов и ТЗ построить — НЕВОЗМОЖНО.

Можно немного побыть Сократом (или троллем, кому как нравится) и спросить: «а что такое правильная архитектура»? Боюсь, ответить не сможет никто. Мне кажется, на самом деле каждый программист в глубине души знает, что его архитектура «неправильная». Я не встречал еще более-менее долгоживущих проектов с «правильной» архитектурой. Архитектура — это всегда компромиссы.

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

В конце концов, если мы будем долго тыкать пальцем в небо, со временем мы сможем составить карту дырок.

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

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

  • Отсутствие состояния;
  • Отсутствие побочных эффектов.

Такие функции зависят только от собственных параметров и не делают ничего кроме вычисления собственного результата.

Что проще тестировать — функцию, которая принимает числовой параметр, преобразует его в строку и возвращает эту строку как результат или функцию, которая берет число из поля класса, преобразует его в строку и сохраняет результат в базе данных? Очевидно, первую функцию. Вторая нарушает сразу оба принципа — она и имеет побочные эффекты (работает с БД), и зависит от некоего состояния (поле класса).

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

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

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

Так мы приходим к анемичной модели, сильно не любимой Мартином Фаулером. Анемичная модель подразумевает разделение классов на классы данных и классы-сервисы. Этот подход расходится с классикой ООП, рекомендующей совмещать данные и методы их обработки в одних и тех же классах. Поэтому Фаулер анемичную модель не любит (мне кажется, что эти его взгляды немного устарели).

Но мы здесь говорим не о некой «идеальной архитектуре в вакууме», мы говорим об архитектуре, которую удобно тестировать. Так что я надеюсь, что мистер Фаулер нас простит.

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

  • Изолировать состояние;
  • Изолировать побочные эффекты;
  • Тяготеть к анемичной модели.

Советов может быть много, но они скорее будут касаться каких-то более универсальных соображений. Я же постарался выделить именно те моменты, которые должны упростить внедрение юнит-тестирования в действующий проект.