Статьи

Умная структура пакета для улучшения тестируемости

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

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

В зависимости от нашего отношения к Domain-Driven-Design, мы постараемся максимизировать (для богатых, бизнес-ориентированных сущностей) или минимизировать (для анемичных сущностей, построенных только из методов получения и установки) тестовое покрытие. Во втором подходе даже трудно сказать о каких-либо тестах, если только вы не доверяете Java и не хотите проверить, может ли get получить значение, назначенное ранее с помощью вызова set. Для богатых компаний мы определенно хотим проверить правильность бизнес-логики. Но, честно говоря, это почти всегда можно сделать с помощью простых модульных тестов с правильной настройкой макета. В этом слое часто бывают тысячи тестов, поэтому мы хотим, чтобы они были максимально быстрыми. Это отличное поле для фреймворков модульного тестирования! Подождите? Почему вы не хотите тестировать сущности с базой данных? Я могу задать противоположный вопрос — зачем мне это делать? Чтобы проверить, работают ли JPA или любой другой постоянный API? Конечно, всегда есть некоторые действительно сложные запросы, которые должны быть проверены с реальной базой данных внизу. Для этих случаев я буду использовать интеграционные тесты на уровне хранилища. Просто база данных + хранилище + сущности. Но помните об одиночной ответственности. Ваши интеграционные тесты проверяют только запрос — оставьте всю логику сущности для модульных тестов.

Следующий уровень обычно строится из сервисов. В DDD сервисы просто работают с репозиториями для загрузки сущностей и делегирования им всей бизнес-логики обработки. Как вы можете предсказать, эти тесты будут довольно простыми. Как вы думаете, нам нужна база данных здесь? Это обеспечит какую-либо добавленную стоимость? Не думай так. А как насчет второго сценария? Анемичные сущности в нашей модели? Вся логика сконцентрирована в сервисах, поэтому мы должны накапливать тестовое покрытие на этом уровне. Но, как мы уже обсуждали с предметной логикой, это можно сделать без использования внешних ресурсов. Еще раз — все, что нам нужно, это юнит-тест. Так что до сих пор нет базы данных. Мы можем запустить все тесты на основе репозиториев макетов. Нет проблем с управлением наборами данных, приводящими к «ожидаемым 3, но найденным 2» сбоям тестов. Просто потому, что какой-то другой тест совершил еще один заказ на сумму от 200 до 300 долларов. Даже если мы хотим использовать IoC-фреймворк здесь, он может имитировать слой репозитория с имитациями. Без должного отделения от структуры уровня данных будет автоматически загружать репозитории через некоторый механизм сканирования. И это не то, что мы хотим.

Помимо услуг мы обычно размещаем что-то, что позволяет пользователям использовать наше приложение. Мы можем выйти, RESTful API, сервисы SOAP и т. Д. Что здесь важно проверить? Чтобы быть честными с нашими клиентами, мы должны придерживаться заключенного с ними договора. Это целое может быть материалом для отдельного поста в блоге, но сужается до сервисов REST:

«Если вы отправите запрос POST на URL / users, я отвечу списком всех пользователей. Каждый пользователь будет иметь идентификатор в виде целого числа и строку с именем пользователя ».

ОК — это выглядит как контракт. Итак, что мы должны проверить в этом слое? Конечно, если этот контракт действителен. Отправьте HTTP-запрос и проверьте, содержит ли ответ массив пользователей, из которого каждая запись строится из целочисленного идентификатора и строки имени пользователя. Можем ли мы сделать это поверх сервисов-макетов? Конечно 🙂

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

  • уровень данных = модульные тесты для логики и интеграционные тесты с БД для проверки сложных запросов
  • уровень обслуживания = модульные тесты для логических и легких интеграционных тестов без БД для тестирования логики, зависящей от инфраструктуры IoC
  • передний уровень = интеграционные тесты без БД для проверки договора с клиентом

До сих пор мы подробно описывали, что стоит тестировать на разных уровнях. Теперь перейдем к функциональной упаковке. Это определенно помогает сохранить код хорошо организованным, когда он построен в разных бизнес-контекстах. Для больших приложений это то, что позволяет разбить его на множество модулей или даже множество приложений. Без такого размещения функции такие действия потребуют огромного рефакторинга раньше. Но нужно ли это после разделения нашего монолита на приложения? Просто подумайте о запуске нового приложения. Какой будет его базовый пакет? com.my.company.application ? Это не что иное, как пакет функций 🙂 Но остановитесь ли вы на этом базовом пакете, или все же будете разбиваться на слои? Как видите, эти две структуры могут жить вместе.

Для слоистой структуры наше приложение будет выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
com.company.application
                      \.data
                           \.config
                           \.model
                           \.repository
                      \.service
                           \.config
                      \.api
                           \.config
                           \.controller

Для функции на основе мы получим что-то вроде

1
2
3
4
com.company.application
                      \.order
                      \.client
                      \.invoice

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
com.company.application.order
                            \.data
                            \.service
                            \.api
 
com.company.application.client
                             \.data
                             \.service
                             \.api
 
com.company.application.invoice
                              \.data
                              \.service
                              \.api

Подводить итоги. На мой взгляд, упаковка слоем является обязательным условием. Это позволяет нам тестировать каждый слой отдельно и держать наши тесты хорошо организованными. Пакет за функцией действительно полезен в больших проектах. Для микросервисов, которые построены вокруг единого связанного контекста, более детальное разделение может привести к неудобной навигации. Однако код внутри пакета объектов должен быть разбит на слои по той же причине, что и упомянутая выше. В частности, структура на основе слоев Spring Framework помогает нам настроить полезное сканирование компонентов и не заставляет нас настраивать базу данных только потому, что мы хотим начать контекст с двух сервисов. В моем репозитории GitHub https://github.com/jkubrynski/spring-package-structure вы можете найти пример проекта на основе Spring.

Ссылка: Умная структура пакета для улучшения тестируемости от нашего партнера JCG Якуба Кубрински в блоге Java (B) Log .