В этом посте я покажу вам интересную библиотеку под названием 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: