Статьи

Проверка кода и архитектурных ограничений с помощью ArchUnit

Вступление

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

Требуемая зависимость

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

1
2
3
4
5
6
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.13.0</version>
    <scope>test</scope>
</dependency>

Если вы все еще используете JUnit 4, вы должны использовать вместо этого артефакт archunit-junit4.

Создание первого правила ArchUnit

Теперь мы можем начать создавать наше первое правило ArchUnit. Для этого мы создаем новый класс в нашей тестовой папке:

01
02
03
04
05
06
07
08
09
10
@RunWith(ArchUnitRunner.class//only for JUnit 4, not needed with JUnit 5
@AnalyzeClasses(packages = "com.mscharhag.archunit")
public class ArchUnitTest {
 
    // verify that classes whose name name ends with "Service" should be located in a "service" package
    @ArchTest
    private final ArchRule services_are_located_in_service_package = classes()
            .that().haveSimpleNameEndingWith("Service")
            .should().resideInAPackage("..service");
}

С @AnalyzeClasses мы сообщаем ArchUnit, какие пакеты Java следует проанализировать. Если вы используете JUnit 4, вам также нужно добавить бегун ArchUnit JUnit.

Внутри класса мы создаем поле и аннотируем его с помощью @ArchTest. Это наш первый тест.

Мы можем определить ограничение, которое мы хотим проверить, используя свободный API Java ArchUnits. В этом примере мы хотим проверить, что все классы, чье имя оканчивается на Service (например, UserService ), находятся в пакете с именем service (например, foo.bar.service ).

Большинство правил ArchUnit начинаются с селектора, который указывает, какой тип кодовых единиц должен проверяться (классы, методы, поля и т. Д.). Здесь мы используем статический метод classes () для выбора классов. Мы ограничиваем выбор подмножеством классов с помощью метода that () (здесь мы выбираем только те классы, имя которых заканчивается на Service ). С помощью метода should () мы определяем ограничение, которое должно сопоставляться с выбранными классами (здесь: классы должны находиться в пакете службы ).

При запуске этого класса тестов будут выполняться все тесты, помеченные @ArchTest. Тест не пройдёт, если ArchUnits обнаружит классы обслуживания вне пакета обслуживания .

Больше примеров

Давайте посмотрим на еще несколько примеров.

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

1
2
3
4
5
6
7
// verify that logger fields are private, static and final
@ArchTest
private final ArchRule loggers_should_be_private_static_final = fields()
        .that().haveRawType(Logger.class)
        .should().bePrivate()
        .andShould().beStatic()
        .andShould().beFinal();

Здесь мы выбираем поля типа Logger и определяем несколько ограничений в одном правиле.

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

1
2
3
4
5
// methods in classes whose name ends with "Util" should be static
@ArchTest
static final ArchRule utility_methods_should_be_static = methods()
        .that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Util")
        .should().beStatic();

Чтобы убедиться, что пакеты с именем impl не содержат интерфейсов, мы можем использовать следующее правило:

1
2
3
4
5
// verify that interfaces are not located in implementation packages
@ArchTest
static final ArchRule interfaces_should_not_be_placed_in_impl_packages = noClasses()
        .that().resideInAPackage("..impl..")
        .should().beInterfaces();

Обратите внимание, что мы используем noClasses () вместо classes (), чтобы отменить ограничение must.

(Лично я думаю, что это правило было бы намного легче читать, если бы мы могли определить правило как interfaces (). Should (). NotResideInAPackage («.. impl ..»). К сожалению, ArchUnit не предоставляет метода interfaces ())

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

1
2
3
4
@ArchTest
static final ArchRule only_repositories_should_use_entityManager = noClasses()
        .that().resideOutsideOfPackage("..repository")
        .should().dependOnClassesThat().areAssignableTo(EntityManager.class);

Пример многоуровневой архитектуры

ArchUnit также поставляется с некоторыми утилитами для проверки определенных стилей архитектуры.

Например, можем ли мы использовать LayeredArchitecture () для проверки правил доступа к слоям в многоуровневой архитектуре:

1
2
3
4
5
6
7
8
@ArchTest
static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
        .layer("Controllers").definedBy("com.mscharhag.archunit.layers.controller..")
        .layer("Services").definedBy("com.mscharhag.archunit.layers.service..")
        .layer("Repositories").definedBy("com.mscharhag.archunit.layers.repository..")
        .whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
        .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
        .whereLayer("Repositories").mayOnlyBeAccessedByLayers("Services");

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

Ярлыки для общих правил

Чтобы избежать того, что мы должны сами определять все правила, ArchUnit поставляется с набором общих правил, определенных как статические константы. Если эти правила соответствуют нашим потребностям, мы можем просто назначить их полям @ArchTest в нашем тесте.

Например, мы можем использовать предопределенное правило NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS, если мы удостоверимся, что не генерируются исключения типа Exception и RuntimeException:

1
2
@ArchTest
private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;

Резюме

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

Как всегда вы можете найти источники из примеров на GitHub . Если вы заинтересованы в ArchUnit, вам также следует ознакомиться с полным руководством пользователя .

Опубликовано на Java Code Geeks с разрешения Михаэля Шаргага, партнера нашей программы JCG. Смотрите оригинальную статью здесь: Проверка кода и архитектурных ограничений с помощью ArchUnit

Мнения, высказанные участниками Java Code Geeks, являются их собственными.