[Перевод] Внедрение рекомендаций по структуре кода с использованием ArchUnit

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

Например, обычно я хочу следовать приведенным ниже рекомендациям для моих приложений на основе Java:

  1. Следуйте трехуровневой структуре (уровни веб, сервис, репозиторий), где любой уровень может взаимодействовать только с непосредственным нижним уровнем, а нижний уровень не должен взаимодействовать с верхним уровнем. т.е. веб-уровень может взаимодействовать с уровнем сервиса, уровень сервиса может взаимодействовать с уровнем репозитория. Но уровень репозитория не может взаимодействовать с сервисным или веб-уровнем, сервисный уровень не может взаимодействовать с веб-уровнем.

  2. Если приложение большое, мы могли бы захотеть следовать структуре Package-By-Feature, где только компоненты Web и Service являются public, а остальные компоненты должны быть package-private.

  3. При использовании внедрения зависимостей Spring не используйте внедрение на основе поля и предпочитайте внедрение на основе конструктора.

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

3b69d0c9f45658a0b64da7cb07e0b7ec.png

Вот здесь руководство пользователя ArchUnit.

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

Добавьте следующую зависимость archunit-junit5.


    com.tngtech.archunit
    archunit-junit5
    0.13.1
    test

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

Правило 1. Сервисы и репозитории не должны взаимодействовать с веб-уровнем.

package com.sivalabs.moviebuffs;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

class ArchTest {

    @Test
    void servicesAndRepositoriesShouldNotDependOnWebLayer() {
      JavaClasses importedClasses = new ClassFileImporter()
          .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
          .importPackages("com.sivalabs.moviebuffs");

      noClasses()
          .that().resideInAnyPackage("com.sivalabs.moviebuffs.core.service..")
            .or().resideInAnyPackage("com.sivalabs.moviebuffs.core.repository..")
          .should()
            .dependOnClassesThat()
            .resideInAnyPackage("com.sivalabs.moviebuffs.web..")
          .because("Services and repositories should not depend on web layer")
          .check(importedClasses);
    }
}

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

Правило 2: следует придерживаться многоуровневой архитектуры

В типичном приложении SpringBoot уровень сервиса зависит от уровня репозитория, а уровни веб и конфигурации зависят от уровня сервиса. Мы не хотим, чтобы уровень Web или Config напрямую взаимодействовал с уровнем репозитория.

Мы можем проверить выполение этого ограничения с помощью следующего теста.

@Test
void shouldFollowLayeredArchitecture() {
  JavaClasses importedClasses = new ClassFileImporter()
          .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
          .importPackages("com.sivalabs.moviebuffs");

  layeredArchitecture()
      .layer("Web").definedBy("..web..")
      .layer("Config").definedBy("..config..")
      .layer("Service").definedBy("..service..")
      .layer("Persistence").definedBy("..repository..")

      .whereLayer("Web").mayNotBeAccessedByAnyLayer()
      .whereLayer("Service").mayOnlyBeAccessedByLayers("Config", "Web")
      .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
      .check(importedClasses);
}

Правило 3: Spring @Autowired НЕ следует использовать с внедрением зависимости на основе поля

@Test
void shouldNotUseFieldInjection() {
    JavaClasses importedClasses = new ClassFileImporter()
          .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
          .importPackages("com.sivalabs.moviebuffs");

    noFields()
      .should().beAnnotatedWith(Autowired.class)
      .check(importedClasses);
}

Правило 4: следует соблюдать соглашение об именах

Мы хотим следовать некоторым соглашениям об именах, например, все имена классов сервиса должны оканчиваться на Service и т.д.

@Test
void shouldFollowNamingConvention() {
    JavaClasses importedClasses = new ClassFileImporter()
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
        .importPackages("com.sivalabs.moviebuffs");
    classes()
        .that().resideInAPackage("com.sivalabs.moviebuffs.core.repository")
        .should().haveSimpleNameEndingWith("Repository")
        .check(importedClasses);

    classes()
        .that().resideInAPackage("com.sivalabs.moviebuffs.core.service")
        .should().haveSimpleNameEndingWith("Service")
        .check(importedClasses);
}

Правило 5: следует использовать только JUnit 5

Мы хотим использовать JUnit 5 в качестве нашей среды тестирования. Но зависимость JUnit 4 могла попасть в путь к классам как транзитивная зависимость (хм… Testcontainers … хм), и мы могли случайно импортировать классы/аннотации JUnit4 ,  такие как @Test ,  Assert и т.д. по ошибке.

Мы можем ограничить использование классов JUnit 4 следующим образом:

@Test
void shouldNotUseJunit4Classes() {
    JavaClasses classes = new ClassFileImporter()
        .importPackages("com.sivalabs.moviebuffs");

    noClasses()
        .should().accessClassesThat().resideInAnyPackage("org.junit")
        .because("Tests should use Junit5 instead of Junit4")
        .check(classes);

    noMethods().should().beAnnotatedWith("org.junit.Test")
        .orShould().beAnnotatedWith("org.junit.Ignore")
        .because("Tests should use Junit5 instead of Junit4")
        .check(classes);
}

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

Пожалуйста, прочтите официальный ArchUnit UserGuide и узнайте о том, какие крутые вещи вы можете делать с ArchUnit.

© Habrahabr.ru