1. Overview
In this article, we'll show how to check the architecture of a system using ArchUnit
.
2. What Is ArchUnit?
The link between architecture traits and maintainability is a well-studied topic in the software industry. Defining a sound architecture for our systems is not enough, though. We need to verify that the code implemented adheres to it.
Simply put, ArchUnit
is a test library that allows us to verify that an application adheres to a given set of architectural rules . But, what is an architectural rule? Even more, what do we mean by architecture
in this context?
Let's start with the latter. Here, we use the term architecture
to refer to the way we organize the different classes in our application into packages .
The architecture of a system also defines how packages or groups of packages – also known as layers
– interact. In more practical terms, it defines whether code in a given package can call a method in a class belonging to another one. For instance, let's suppose that our application's architecture contains three layers: presentation
, service
, and persistence
.
One way to visualize how those layers interact is by using a UML package diagram with a package representing each layer:
Just by looking at this diagram, we can figure out some rules:
- Presentation classes should only depend on service classes
- Service classes should only depend on persistence classes
- Persistence classes should not depend on anyone else
Глядя на эти правила, мы можем теперь вернуться и ответить на наш первоначальный вопрос. В этом контексте архитектурное правило — это утверждение о том, как наши классы приложений взаимодействуют друг с другом.
Итак, как нам проверить, что наша реализация соблюдает эти правила? Вот тут-то и появляется ArchUnit
. Он позволяет нам выражать наши архитектурные ограничения с помощью гибкого API
и проверять их вместе с другими тестами во время обычной сборки.
3. Настройка проекта ArchUnit
ArchUnit
хорошо интегрируется с тестовой средой JUnit
, поэтому они обычно используются вместе. Все, что нам нужно сделать, это добавить зависимость archunit-junit4
в соответствии с нашей версией JUnit :
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
Как следует из его артефакта
, эта зависимость специфична для платформы JUnit
4.
Также существует зависимость archunit-junit5
, если мы используем JUnit
5:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
4. Написание тестов ArchUnit
После того, как мы добавили соответствующую зависимость в наш проект, давайте начнем писать наши архитектурные тесты. Наше тестовое приложение будет простым приложением SpringBoot REST, которое запрашивает Smurfs . Для простоты это тестовое приложение содержит только классы Controller
, Service
и Repository .
Мы хотим убедиться, что это приложение соответствует упомянутым ранее правилам. Итак, начнем с простого теста на правило «классы представления должны зависеть только от классов обслуживания».
4.1. Наш первый тест
Первым шагом является создание набора классов Java, которые будут проверяться на предмет нарушений правил . Мы делаем это, создавая экземпляр класса ClassFileImporter
, а затем используя один из его методов importXXX()
:
JavaClasses jc = new ClassFileImporter()
.importPackages("com.foreach.archunit.smurfs");
В этом случае экземпляр JavaClasses
содержит все классы из нашего основного пакета приложения и его подпакетов. Мы можем думать об этом объекте как о типичном испытуемом, используемом в обычных модульных тестах, поскольку он будет целью для оценки правил.
Архитектурные правила используют один из статических методов класса ArchRuleDefinition
в качестве отправной точки для вызовов API Fluent.
Давайте попробуем реализовать первое правило, определенное выше, используя этот API. Мы будем использовать метод class()
в качестве нашего якоря и добавим оттуда дополнительные ограничения:
ArchRule r1 = classes()
.that().resideInAPackage("..presentation..")
.should().onlyDependOnClassesThat()
.resideInAPackage("..service..");
r1.check(jc);
Обратите внимание, что нам нужно вызвать метод check()
созданного нами правила, чтобы запустить проверку. Этот метод принимает объект JavaClasses
и выдает исключение в случае нарушения.
Все это выглядит хорошо, но мы получим список ошибок, если попытаемся запустить его для нашего кода:
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 библиотеки
, набор предварительно упакованных правил , решающих общие проблемы архитектуры
:
Архитектуры
: поддержка проверки правил многоуровневой и луковой (также известной как Hexagonal или «порты и адаптеры») архитектур.Срезы
: используются для обнаружения круговых зависимостей или «циклов».Общие
: набор правил, связанных с лучшими практиками кодирования, такими как ведение журнала, использование исключений и т. д.PlantUML
: проверяет, соответствует ли наша кодовая база заданной модели UML.Правила Freeze Arch
: сохраняйте нарушения для дальнейшего использования, позволяя сообщать только о новых. Особенно полезно для управления техническими долгами
Рассмотрение всех этих правил выходит за рамки данного введения, но давайте взглянем на пакет правил архитектуры .
В частности, давайте перепишем правила из предыдущего раздела, используя правила многоуровневой архитектуры. Использование этих правил требует двух шагов: во-первых, мы определяем слои нашего приложения. Затем мы определяем, к какому слою разрешен доступ:
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 .