«1. Обзор

В этой статье мы покажем, как проверить архитектуру системы с помощью ArchUnit.

2. Что такое ArchUnit?

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

Проще говоря, ArchUnit — это тестовая библиотека, которая позволяет нам проверять, соответствует ли приложение заданному набору архитектурных правил. Но что такое архитектурное правило? Более того, что мы подразумеваем под архитектурой в этом контексте?

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

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

Один из способов визуализировать взаимодействие этих слоев — использовать диаграмму пакетов UML с пакетом, представляющим каждый уровень:

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

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

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

3. Настройка проекта ArchUnit

ArchUnit прекрасно интегрируется с тестовой средой JUnit, поэтому они обычно используются вместе. Все, что нам нужно сделать, это добавить зависимость archunit-junit4, чтобы она соответствовала нашей версии JUnit:

Как следует из ее артефактаId, эта зависимость специфична для среды JUnit 4.

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>0.14.1</version>
    <scope>test</scope>
</dependency>

Существует также зависимость archunit-junit5, если мы используем JUnit 5:

4. Написание тестов ArchUnit

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.14.1</version>
    <scope>test</scope>
</dependency>

После того, как мы добавили соответствующую зависимость в наш проект, давайте начнем писать наши архитектурные тесты. Наше тестовое приложение будет простым приложением SpringBoot REST, которое запрашивает Smurfs. Для простоты это тестовое приложение содержит только классы Controller, Service и Repository.

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

4.1. Наш первый тест

Первым шагом является создание набора классов Java, которые будут проверяться на предмет нарушений правил. Мы делаем это, создавая экземпляр класса ClassFileImporter, а затем используя один из его методов importXXX():

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

JavaClasses jc = new ClassFileImporter()
  .importPackages("com.baeldung.archunit.smurfs");

Архитектурные правила используют один из статических методов класса ArchRuleDefinition в качестве отправной точки для своих плавных вызовов API. Давайте попробуем реализовать первое правило, определенное выше, используя этот API. Мы будем использовать метод class() в качестве привязки и добавим оттуда дополнительные ограничения:

Обратите внимание, что для запуска проверки нам нужно вызвать метод check() созданного нами правила. Этот метод принимает объект JavaClasses и выдает исключение в случае нарушения.

ArchRule r1 = classes()
  .that().resideInAPackage("..presentation..")
  .should().onlyDependOnClassesThat()
  .resideInAPackage("..service..");
r1.check(jc);

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

«

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - 
  Rule 'classes that reside in a package '..presentation..' should only 
  depend on classes that reside in a package '..service..'' was violated (6 times):
... error list omitted

«Почему? Основная проблема с этим правилом — это onlyDependsOnClassesThat(). Несмотря на то, что мы поместили на диаграмму пакета, наша фактическая реализация зависит от классов JVM и Spring framework, отсюда и ошибка.

4.2. Переписываем наш первый тест

Один из способов решить эту ошибку — добавить предложение, учитывающее эти дополнительные зависимости:

ArchRule r1 = classes()
  .that().resideInAPackage("..presentation..")
  .should().onlyDependOnClassesThat()
  .resideInAPackage("..service..", "java..", "javax..", "org.springframework..");

С этим изменением наша проверка перестанет давать сбои. Этот подход, однако, страдает от проблем с ремонтопригодностью и кажется немного хакерским. Мы можем избежать этих проблем, переписав наше правило, используя статический метод noClasses() в качестве отправной точки:

ArchRule r1 = noClasses()
  .that().resideInAPackage("..presentation..")
  .should().dependOnClassesThat()
  .resideInAPackage("..persistence..");

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

5. Использование API библиотеки

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

    Архитектуры: поддержка многоуровневых и луковичных (также известных как гексагональные или «порты и адаптеры») архитектур проверки правил. Срезы: используются для обнаружение циклических зависимостей, или «циклов». Общее: набор правил, связанных с лучшими практиками кодирования, такими как ведение журнала, использование исключений и т. д. PlantUML: проверяет, соответствует ли наша кодовая база заданной модели UML. использование, позволяющее сообщать только о новых. Особенно полезно для управления техническими долгами

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

LayeredArchitecture arch = layeredArchitecture()
   // Define layers
  .layer("Presentation").definedBy("..presentation..")
  .layer("Service").definedBy("..service..")
  .layer("Persistence").definedBy("..persistence..")
  // Add constraints
  .whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
  .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
  .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
arch.check(jc);

Здесь layeredArchitecture() — это статический метод из класса Architectures. При вызове он возвращает новый объект LayeredArchitecture, который мы затем используем для определения имен слоев и утверждений относительно их зависимостей. Этот объект реализует интерфейс ArchRule, поэтому мы можем использовать его так же, как и любое другое правило.

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

6. Заключение

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

Как обычно, весь код доступен на GitHub.