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

Подход, который я долгое время использовал массово - написание юнит-тестов с использованием Mockito. В заглушки превращается любой сторонний сервис, используемый тестируемым классом. Тесты супер-быстрые, все зеленое, все супер!

Со временем я заметил, что этот подход начал изрядно напрягать. Часто на две строчки кода приходилось писать не менее 10-20 строк теста. Огромное количество переопределений поведения заглушек. Проверки, что тот или иной метод сторонних сервисов более не вызывался или вызывался не более определенного количества раз. Без преувеличения, огромное количество бойлер-плейт кода в тестах.

Дальше - больше. Множество зеленых галочек, появляющееся на экране при запуске тестов, не могло не радовать. Но вот ты решаешь сделать минимальный рефакторинг, оптимизировать какой-то метод. В этот момент все разваливается на куски. Куча красных тестов! Как, почему? Да просто во внутренней реализации ты перешел на использование другого метода, добавил в метод еще один параметр или что-то подобное. В результате куча тестов перестала работать. И теперь, поменяв одну строчку в процессе рефакторинга, надо поменять еще 30 строк в тестах. В какой-то момент начинаешь ловить себя на мысли - “Ни в коем случае, никаких рефакторингов больше!”

Вишенка на торте - ты выкатываешь приложение (нет, не в Прод, а только на тестовый стенд) и бац - ошибки. Приложение просто не стартует. Или стартует, но запрос, так успешно протестированный, ничего не возвращает. А ведь ты уже отчитался, что два дня писал тесты и все у тебя в порядке. В чем же дело? Дело в том, что дьявол кроется в деталях. Где-то перепутал нейминг в настройках, написал некорректный JPQL-запрос, огромное количество реальных деталей, ошибившись в которых, несмотря на пару сотен зеленых и быстрых юнит-тестов, у тебя ничерта не работает и совершенно непонятно, где искать проблему!

Я начал искать выход из ситуации и нашел. В этом мне очень помогла потрясающая книга Владимира Хорикова “Принципы юнит-тестирования” и несколько видео от Victor Rentea, посвященных написанию тестов.

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

Да, это несколько дольше запускается, чем классические юнит-тесты. Зато такой подход очень устойчив к рефакторингу, позволяет выявить огромное количество неочевидных проблем, таких, к примеру, как ошибки в конфигурации продюсеров и консюмеров или REST-клиентов, ошибки в запросах SQL и JPQL, ошибки сериализации и так далее. Да просто обновите версию очередной библиотеки и если что-то пошло не так - вы это сразу увидите. Кроме того, так можно проверить действительно сложные головоломные кейсы, которые крайне сложно проверить на стандартных моках из Mockito.

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

Хочу отметить, что решения, показанные в тестовом приложении, это не example и не tutorial. Это вполне production-ready реализация. Ключевая цель этого доклада - дать разработчику максимально готовый инструмент, которым он может воспользоваться быстро и максимально быстро преодолеть порог вхождения в тестирование с помощью Testcontainers. Надеюсь, что я с этой задачей справился, но решать вам :) Поехали!

Тестовое приложение

Итак, вот что представляет из себя приложение, которое мы будем тестировать:

  1. Java 21, Spring Boot 3.5.9

  2. СУБД - PostgreSQL

  3. Для миграций БД - Liquibase

  4. Принимает входящие REST-запросы

  5. Отправляет REST-запросы во внешние системы

  6. Принимает сообщения из разных кластеров Кафки

  7. Отправляет сообщения в разные кластера Кафки

Так как подключение к базе у нас одно, то его настройки отдадим Spring Boot (через spring.datasource), а вот подключение к кластерам Кафки реализуем вручную. Делаю на этом акцент, так как в дальнейшем это окажет влияние на реализацию настроек приложения.

Приложение для примера: https://gitflic.ru/project/leva1981/testcontainers-variations

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

Что и как будем тестировать

Подход к тестированию следующий:

  • Все необходимое запускается автоматически при запуске тестов.

  • Запускается вся необходимая для работы нашего приложения инфраструктура - СУБД, кластера Кафки, имитация внешних web-серверов.

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

  • Для тестирования функционала делаем обращение к нашему приложению через REST или кладем сообщения в топики, которые слушает наше приложение. Затем проверяем результат - это может быть ответ по REST, обновление данных в БД, появление сообщений в исходящих топиках Кафки и так далее.

Такие тесты пишем на все основные сценарии использования. На логику, не связанную с интеграциями или на сложно-тестируемые corner-case, напишем дополнительные init-тесты с использованием Mockito. Наша цель - покрыть интеграционными тестами все основные сценарии, а затем довести покрытие всех классов с логикой до 90-100%.

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

Функция

Что надо сделать

Работа с СУБД

Поднять PostgreSQL

Заполнить базу тестовыми данными

Настроить наше приложение на подключение к этому экземпляру СУБД

Обработка входящих REST-запросов

Запустить приложение на случайном порту, чтобы избежать пересечений с другими приложениями

Запрос по REST во внешнюю систему

Поднять имитацию веб-сервера внешней системы - Wiremock

Настроить моки ответов

Настроить наше приложение на адрес и порт имитации

Отправка сообщения в Кафку

Поднять кластер Кафки

Настроить наше приложение на подключение к этому экземпляру Кафки для отправки сообщений

Получение сообщений из Кафки

Для упрощения будем подключаться к тому же экземпляру Кафки, что и для отправки сообщений

Настроить наше приложение на подключение к этому экземпляру Кафки для получения сообщений

Поднимаем тестовое окружение

  1. Зависимости (testcontainers, rest-assured, json-unit, ...). Настройка корпоративного docker registry при необходимости.

  2. application-test.yaml - меняем уровень логирования, можем установить другие настройки для тестов

  3. TestApplicationInitializer

    1. контейнер для Postgres - дополнительно указываем скрипт создания схемы. Тестовые данные прольем позже, после того как Спринг запустит liquibase и будет создана структура БД.

    2. связка WireMockContainer + WireMock. Первое - контейнер, второе - клиент для управления моками в контейнере. Делаем столько контейнеров, сколько у нас интеграций.

    3. initialize() - поднимаем все контейнеры, инициализируем клиентов Wiremock, устанавливаем свойства приложения, создаем топики Кафки и заполняем базу тестовыми данными.

  4. AbstractIntegrationTest - сюда выносим все общие объекты, чтобы избежать повторного поднятия Spring Test Context, а так же инициализируем базовый путь для rest-assured.

  5. Пробуем запуститься - TestTestcontainersVariationsApplication

    1. дорабатываем класс чтобы использовать TestApplicationInitializer

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

Замечание по rest-assured. В Spring Boot 4 Test уже есть встроенное решение, очень похожее на rest-assured. Но так как тестовое приложение написано с использованием SB3.5, то пока используем rest-assured.

Собственно, если мы смогли запустить TestTestcontainersVariationsApplication, значит конфигурирование тестового окружения можно считать успешным. Теперь можно запускать TestTestcontainersVariationsApplication в debug-режиме, тыкать наше приложение палочкой снаружи и смотреть, что из этого выходит. Первый этап выполнен!

Дополнительные приемы

Оставляем контейнеры работать

В TestApplicationInitializer выставляем REUSE_CONTAINERS = true. Это приведет к тому, что между перезапусками тестового приложения (или тестов) контейнеры не будут гаситься. То есть можно сэкономить время на перезапусках.

Обратите внимание на комментарий над константой REUSE_CONTAINERS - понадобится внести небольшое изменение в файл ~/.testcontainers.properties. ~ это каталог текущего пользователя, например, в моем случае это C:\Users\lsukhin.

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

Мягкий перезапуск через devtools

Так как к проекту подключена зависимость spring-boot-devtools, при изменении одного или нескольких классов можно выполнить повторную компиляцию и произойдет автоматический мягкий рестарт приложения. См. документацию к devtools и README.md тестового приложения.

Пишем тесты

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

  1. PaymentServiceTests - делаем запрос в наш сервис, он в свою очередь выполняет rest-запрос во внешний сервис, получает ответ и возвращает нам результат

    1. регистрируем ответ от внешнего web-сервиса

    2. загружаем ожидаемый ответ

    3. делаем вызов

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

    5. сравниваем ответ с ожидаемым ответом

  2. OrderServiceTests

    1. загружаем ожидаемый ответ

    2. делаем вызов

    3. сравниваем ответ с ожидаемым ответом

    4. используя Awaitity.await() проверяем, создалась ли запись в БД и был ли получен ответ от внешнего сервиса через Кафку (что приводит к смене статуса заказа с CREATED на VERIFIED)

  3. sendOrderCreatedEvent_whenExternalServiceKafkaDisabled_dontSendEvents - дополнительный unit-тест на логику с использованием доступа к private-методу, который сделаем protected + @VisibleForTesting

  4. TestcontainersVariationsApplicationTests - визуально проверяем, что поднимается только один контекст Спринг

Готово! Теперь можем запустить тесты через Идею с анализом тестового покрытия и оценить, насколько успешно мы покрыли наше приложение тестами.

Как еще можно

Spring way - поднимаем контейнеры через бины

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

Для того, чтобы перевести демонстрационное приложение на использование бинов тест-контейнеров, надо раскомментировать/закомментировать несколько строк в файлах TestTestcontainersVariationsApplication и AbstractIntegrationTest.

В этом случае TestApplicationInitializer нам будет не нужен, для конфигурирования будут задействованы TestApplicationConfiguration и TestApplicationConfigurationMocks.

Использование docker-compose

Можно столкнуться с ситуацией, когда совершенно некогда возиться с тест-контейнерами, а приложение надо по-быстрому поднять локально и подебажить. К примеру, это может произойти, когда вам надо быстренько что-то поправить в микросервисе, за который отвечали не вы. В этом случае вы сможете быстренько поднять всю инфраструктуру через docker compose. Я сделал заготовки, которые должны максимально облегчить эту задачу. См. файлы в папке ./docker. Подробное описание расположено в файле ./docker/README.md

Оба файла (docker-compose.yml, docker-admin-ui.yml) используют переменные окружения, определенные в файле .env, для настройки портов, версий образов и других параметров.

docker-compose.yml

Файл docker-compose.yml содержит конфигурацию для запуска нескольких сервисов, необходимых для работы приложения:

kafka-externalservice: Сервис Kafka для обработки сообщений. Настроен с внешними и внутренними портами, а также с переменными окружения для конфигурации брокера и контроллера.

postgres: Сервис PostgreSQL для хранения данных. Использует переменные окружения для настройки пользователя, пароля и имени базы данных. Содержит том для сохранения данных и скрипт инициализации схемы.

wiremock: Сервис WireMock для создания моков HTTP-запросов. Настроен с портом и монтированием директорий для файлов и маппингов.

db-migration: Сервис для применения миграций базы данных с использованием Liquibase. Запускается после успешного запуска PostgreSQL и использует переменные окружения для подключения к базе данных и указания файла миграций.

db-migration-rollback: Сервис для отката миграций базы данных с использованием Liquibase. Аналогично db-migration, но выполняет команду отката до указанной даты.

database-load-test-data: Сервис для загрузки тестовых данных в базу данных PostgreSQL. Использует скрипт _run_scripts.sh для выполнения SQL-запросов.

docker-admin-ui.yml

Файл docker-admin-ui.yml содержит конфигурацию для запуска UI-интерфейсов для управления сервисами:

redis-ui: Интерфейс RedisInsight для управления Redis. Настроен с портом и переменными окружения для подключения к Redis.

kafka-ui: Интерфейс Kafka UI для управления Kafka. Настроен с портом и переменными окружения для подключения к Kafka, включая имя кластера и адрес брокера.

pgadmin: Интерфейс pgAdmin для управления PostgreSQL. Настроен с портом и переменными окружения для подключения к базе данных, включая электронную почту и пароль администратора. Также содержит конфигурационные файлы для серверов и подключения к базе данных.

Получаем разрешение на освоение и внедрение

Итак, вы обогатились теоретическими знаниями. Теперь надо перевести знания в практическую плоскость и внедрить в один из сервисов вашего проекта (для начала). Хотелось бы делать это не в свободное время, а вполне официально.

Вопрос, который вам сразу зададут, будет скорее всего звучать так - “Сколько времени тебе на это нужно?”. Давайте прикинем. На повторный анализ статьи, перенос кода в свой сервис, танцы с бубном при настройке и запуске - на это уйдет рабочий день. Еще один день уйдет на написание первого интеграционного теста. Потом вы переспите со всей этой новой информацией и, скорее всего, доработаете свой первый тест или напишите еще один. То есть может добавиться (или нет) еще один рабочий день. На этом этап освоения можно считать законченным. То есть на вопрос “Сколько времени тебе нужно?” можно смело отвечать “Два, максимум три, рабочих дня.” Срок адекватный, поэтому с большой вероятностью вы сможете завести себе задачу и разобраться с вопросом в рабочее время.

Подведем итоги

Testcontainers решает ту самую боль, с которой я столкнулся, имея дело с сотнями “зеленых” Mockito-тестов: все тесты проходят, но в реальности приложение не работает. Вместо того чтобы тестировать изолированные классы с кучей моков, мы тестируем настоящую функциональность с реальной PostgreSQL, Kafka и другими внешними сервисами. Конечно это медленнее, чем юнит-тесты, но зато когда тесты проходят - приложение действительно работает. Это плата за спокойствие.

Вы перестанете бояться рефакторинга, вы сами (и ваша команда) перестанете спрашивать: “Почему тесты проходят и локально работает, а на стенде падает?” Это не просто инструмент - это смена подхода к тестированию, которая экономит часы отладки интеграционных проблем и дает настоящую уверенность в коде. Это реальная эволюция в подходе к качеству. Это шаг от вопроса “работает ли этот метод?” к вопросу “работает ли мое приложение?”.

Материалы

Приложение:

Использованные материалы:

Автор

Леонид Сухин, Java-разработчик
Telegram: @levaryazan

Комментарии (9)


  1. mihun4ik
    20.01.2026 07:24

    Очень классная статья, прям чувствуется, что написана человеком, который реально этим пользуется, а не просто пересказывает доку. И видно, что большой труд проделан. Спасибо автору!

    Есть вопрос. Насколько ваш репо готов к использованию в качестве образца «как надо» для LLM-моделей?

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

    P.S. Кстати, а под какой лицензией ваш код — можно ли его использовать в личных проектах?)


    1. leva1981 Автор
      20.01.2026 07:24

      По первому вопросу - у меня мало опыта "натравливания" LLM на репозитории)) Наверно наличие фрагментов кода под комментами (ИЛИ-ИЛИ) действительно может создать некоторые затруднения. Но мне реально не хотелось делать две разные ветки, которые бы отличались буквально четырьмя строками кода и наличием/отсутствием одного-двух файлов. Я оставил все в одной куче специально, чтобы было сразу видно в чем собственно отличия. Наверно есть смысл форкнуть, удалить лишнее и уже после этого натравить LLM.

      По второму вопросу - можно использовать в любых проектах.


  1. DenSigma
    20.01.2026 07:24

    Статья интересная, но полностью противоречит идеям Хорикова.


    1. leva1981 Автор
      20.01.2026 07:24

      Возможно вы слишком категоричны. Можете развернуть комментарий?


      1. DenSigma
        20.01.2026 07:24

        Насколько я понял идеи Хорикова, он жестко разделяет слои доступа к базе данным и бизнес-логики. доступ к базе данных (и доступ к другим сервисам, если не путаю) необходимо реализовывать в виде "Простых объектов" - не содержащих бизнес-логики классов. Настолько простых, что там нечего тестировать. Что необходимо покрывать тестами - это расчеты, решения, бизнес-логику.

        Если мы расчитываем продажи, то покрывать тестами надо классы, которые умножают количество проданного товара на цену, суммирует записи, и/или накладывает скидки и прочее по каким-то условиям. Вот этот класс, кроме как вычисляет, не должен обращаться к базе данных и прочим сервисам. Его и кроем тестами. Этот класс возвращает данные в виде простого объекта с полями и массивами чего-то. Данные возвращаются в "простой класс", который один к одному сохраняет данные в базу. Вот этот простой объект мы уже НЕ КРОЕМ тестами. Там нечего тестировать и нет смысла покрывать тестами.

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

        А вы кроете свои сложные классы (я знаю, что это такое, когда по ходу расчетов наш класс обращается к базе) тестами, прикручиваете прорву моков, которые на запросы к базе возвращают кучу данных и на методы сохранения в базу прикручиваете моки, которые проверяют, что вы пытаетесь сохранить а базу. Еще и пытаетесь наверное, протестировать, как sql-ки формируются.

        Прекрасно понимаю, что это полная жопа.

        И как решение, предлагаете тест-контейнеры... Они вам не помогут.

        Апд. В идеальном случае, кстати, при таком подходе, результат расчета сохраняется в виде документа в NoSQL базу или в JSON-поле за один вызов.


        1. leva1981 Автор
          20.01.2026 07:24

          Ок, давайте подискутируем. У Хорикова описывается довольно много различных подходов, а также их плюсы и минусы. Как мне показалось, одна из ключевых мыслей Хорикова - что необходимо уйти от хрупких тестов Лондонской школы, когда мокается все и вся и по возможности использовать тесты, которую тестируют ваше приложение по принципу "черного ящика" и тестируют не классы, а определенный функционал. Но повторюсь - в книге разобрано много кейсов и мой ключевой вывод это только мой вывод.

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

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

          Теперь про подход.

          Пишем интеграционные тесты на все happy path. С поднятием инфры через тест-контейнеры и так далее. Так ты точно знаешь, что все связки отрабатывают насквозь, по крайней мере по одному положительному сценарию. Это кстати уже обычно дает примерно 65-70% покрытия кода.

          Затем по мере возможности ты выносишь бизнес-логику в отдельные методы. Цель - метод без внешних зависимостей, без внешних сервисов. Такие методы от и до тестируешь через юнит-тесты. Подобные тесты неплохо пишет ИИ, с обработкой кучи корнер-кейсов (только за ИИ обязательно все надо перепроверить). Таким образом прекрасно тестируется вся логика, если вам повезло практиковать DDD-подход. То есть если у тебя есть бизнес-логика без зависимостей, то ее все так же тестируем через юниты. Это очень годно работает для всяких библиотек, которые что-нибудь считают, или там регулярками обрабатывают. Там прямо юниты рулят.

          Что касается тестирования простых объектов. Этого нет в докладе и коде, но да, объекты типа dto, репозитории и тому подобное необходимо вообще исключать из анализа тестового покрытия, включать их в исключения JaCoCo/Сонара. Это не вошло в реализацию и в доклад.

          Что касается "прикручиваете прорву моков, которые на запросы к базе возвращают кучу данных и на методы сохранения в базу прикручиваете моки, которые проверяют, что вы пытаетесь сохранить а базу. Еще и пытаетесь наверное, протестировать, как sql-ки формируются." Тут уж извините, но вам стоит повнимательнее еще раз прочитать доклад и посмотреть код. В том то и дело, что я не прикручиваю моки. Я поднимаю реальную инфру, которая нужна для функционирования сервиса. Реальный Пострес, реальную Кафку, реальный Редис (тут его нет) и так далее. И потом тестирую максимально реальное поведение сервиса. Мне не надо тестировать "sql-ки формируются" потому что я тестирую конечный результат. Дал на вход определенные параметры, в рест или кафку, и жду определенного результата - появления/изменения данных в базе, исходящего сообщения в очередь, вызова внешнего сервиса... Исходя из того, что вы пишите, создается впечатление, что как в анекоде из СССР - "Пастернака не читал. Но осуждаю". Так же и вы невнимательно по диагонали прочитали статью и судя по всему не удосужились посмотреть код. Пожалуйста, дайте статье и коду второй шанс - приглядитесь. Возможно, вы в итоге до конца не согласитесь с моим подходом и реализацией, но совершенно точно ваша критика будет другой.


          1. DenSigma
            20.01.2026 07:24

            Да, это годные аргументы, соглашусь. Спасибо.


  1. Snaret
    20.01.2026 07:24

    Спасибо за статью)

    Не всегда Вам в pipeline дадут доступ к докеру. В этом случае могу порекомендовать Zonky который поднимет postgresQL в памяти без всяких контейнеров. Удобно и ощутимо быстрее чем с контейнерами.

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


    1. leva1981 Автор
      20.01.2026 07:24

      Я был в такой ситуации и с Zonky, он же EmbeddedPostgres, Embedded... работал.

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

      По покрытию. Посмотрите пожалуйста ответ на коммент выше, там где много букв. Я не предлагаю все методы покрывать интеграционниками. Достаточно happy path по всем сутевым кейсам. Юнит-тесты никто не отменял, через них прекрасно тестируется бизнес логика, если она не содержит зависимостей (DDD, утильные классы и библиотеки, просто отдельные методы с вынесенной в них логикой, ветвления с exception для повышения покрытия по параметру branch). Веду к тому, что и интеграционных тестов и юнитов должно быть ровно столько сколько нужно, чтобы покрыть все, что реально надо покрыть и что-бы эта история была устойчива к рефакторингу, насколько это возможно.