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

Вот список тестов, которые мы напишем в этом посте:

  • Циклический тест зависимости
  • Тест доступа к слою
  • Тест местоположения класса
  • Метод возврата типа Test
  • Тест соглашения об именах

Итак, представим проект со структурой пакета, показанной ниже:

Перед написанием тестов для нашей архитектуры в качестве отправной точки мы решаем, что к нашим контроллерам нельзя обращаться из любого другого класса или пакета. Также концептуально мы принимаем, что имена контроллеров должны заканчиваться суффиксом «… Controller».

Пришло время запачкать руки. Ниже мы приступаем к написанию нашего первого теста. Это позволяет нам проверить наше соглашение об именах.

Тесты на соответствие именованию

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.test.controllers")
public class NamingConventionTests {

    @ArchTest
    ArchRule controllers_should_be_suffixed = classes()
            .that().resideInAPackage("..controllers..")
            .should().haveSimpleNameEndingWith("Controller");
    
}

Когда мы запускаем тест, мы видим, что он проходит:

Есть два типа испытаний с дуговым блоком. Один из них похож на показанный выше. Если мы хотим, мы можем написать тесты, используя аннотацию JUnit Test. Измените параметр RunWith на JUnit4.class и удалите аннотацию AnalyzeClasses.

Таким образом, мы указываем пакеты для импорта с помощью ClassFileImporter в ArcUnit.

@RunWith(JUnit4.class)
public class NamingConventionTests {

    @Test
    public void controllers_should_be_suffixed() {
        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.test.controllers");

        ArchRule rule = classes()
                .that().resideInAPackage("..controllers..")
                .should().haveSimpleNameEndingWith("Controller");

        rule.check(importedClasses);
    }
}

Теперь посмотрим, что произойдет, если у нас будет другой суффикс. Измените ("Controller") to ("Ctrl") и запустите:

Исключение гласит: «java.lang. AssertionError: Нарушение архитектуры [Приоритет: СРЕДНИЙ] - классы правила, которые находятся в пакете« ..controllers .. », должны иметь простое имя, оканчивающееся на Ctrl, было нарушено (1 раз):
простое имя com.test.controllers. FirstController не заканчивается на Ctrl в (FirstController. java: 0) »

Все идет нормально. Мы написали наш первый тест, и он работает правильно. Пришло время перейти к другим тестам.

Тесты определения местоположения

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

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.test")
public class RepositoryPackageTest {

    @ArchTest
    public ArchRule repositories_should_located_in_infrastructure = classes()
            .that().areAnnotatedWith(Repository.class)
            .should().resideInAPackage("..infrastructure..");

}

Если мы аннотируем другие классы, кроме пакетов инфраструктуры, тест вызывает AssertionError.

Тесты возвращаемого типа метода

Напишем несколько проверок методов. Предположим, мы решили, что наши методы контроллера должны возвращать тип BaseResponse.

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.test.controllers")
public class ControllerMethodReturnTypeTest {

    @ArchTest
    public ArchRule controller_public_methods_should_return = methods()
            .that().areDeclaredInClassesThat().resideInAPackage("..controllers..")
            .and().arePublic()
            .should().haveRawReturnType(BaseResponse.class)
            .because("here is the explanation");

}

Циклические тесты зависимости

В наши дни проблемы циклической зависимости решаются большинством контейнеров IOC. Хорошо иметь какой-нибудь инструмент, который проверяет это за нас.

Теперь сначала создайте классы с циклической сложностью:

package com.test.services.slice1;

import com.test.services.slice2.SecondService;

public class FirstService {
    private SecondService secondService;

    public FirstService() {
        this.secondService = new SecondService();
    }
}
package com.test.services.slice2;

import com.test.services.slice1.FirstService;

public class SecondService {
    private FirstService firstService;

    public SecondService() {
        this.firstService = new FirstService();
    }
}

FirstService и SecondService зависят друг от друга, и это создает цикл.

Теперь напишем для него тест:

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.test")
public class CyclicDependencyTest {

    @ArchTest
    public static final ArchRule rule = slices().matching("..services.(*)..")
            .should().beFreeOfCycles();

}

Выполнение этого теста дает следующий результат:

Кроме того, результат такой же, как и при внедрении конструктора.

Тесты слоев

Пришло время написать тест слоя, который покрывает наши слои.

@RunWith(JUnit4.class)
public class LayeredArchitectureTests {

    @Test
    public void layer_dependencies_are_respected() {
        JavaClasses importedClasses = new ClassFileImporter().importPackages("..com.test..");

        ArchRule myRule = layeredArchitecture()
                .layer("Controllers").definedBy("..com.test.controllers..")
                .layer("Services").definedBy("..com.test.services..")
                .layer("Infrastructure").definedBy("..com.test.infrastructure..")
                .whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
                .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
                .whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Services");

        myRule.check(importedClasses);
    }
}

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

package com.test.infrastructure;

import com.test.services.SecondService;

public class FirstRepository {
    SecondService secondService;

    public FirstRepository(SecondService secondService) {
        this.secondService = secondService;
    }
}

Когда мы запустим тест, мы увидим, что наш репозиторий нарушает правила:

Подведение итогов

ArchUnit, как видите, гарантирует, что ваш проект имеет правильную архитектуру. Это помогает поддерживать структуру проекта в чистоте и не дает разработчикам вносить критические изменения.

Мы сделали краткий обзор библиотеки. Помимо всех его функций, я думаю, было бы здорово, если бы в ArchUnit были некоторые правила для тестирования гексагональной архитектуры, cqrs и некоторых концепций DDD, таких как агрегаты, объекты значений и т. Д.

Для любопытных вот код на Github: