Archunit testy architektury

Archunit testy architektury

Do pisania testów raczej nikogo nie trzeba przekonywać. Wielu z Was pisze testy jednostkowe, integracyjne, funkcjonalne. Ale jak można przetestować architekturę i czy to ma sens? Przy pomocy archunit spróbuję odpowiedzieć na te pytania.

Dlaczego

Archunit jest biblioteką którą za pomocą testów można opisać reguły dla architektury naszej aplikacji. Biorąc pod uwagę, że architektura cały czas żyje i się zmienia, to posiadanie jej opisu jako testy może być najlepszą dokumentacją. Dokumentacją, która jest cały czas aktualna i szybko wskaże reguły, które zostały złamane. Także, zbiór jasnych zasad pozwoli oszczędzić trochę czasu podczas code review.

Start

Najlepiej korzystanie z archunit zacząć od  dokumentacji. Biblioteka posiada jary zgodne z junit5 oraz junit 4. Ale nic nie stoi na przeszkodzie aby używać jej również z innymi frameworkami do testów. Kolejnym plusem tego narzędzia jest DSL w jakim testy są napisane oraz łatwość ich czytania. Również ważne jest użycie klas, które opakowują refleksje w javie.

Archunit praktyka

Najłatwiej z biblioteki zacząć poprzez praktyczny przykład. Wobec tego na podstawie architektury portów i adapterów zademonstruję kilka reguł.  Przykładowy projekt wygląda następująco:

archunit projekt dla ports and adapters

Pierwszy test może wyglądać następująco:

@AnalyzeClasses(packages = "pl.socodeit.archunit")
public class ArchitectureTest {

    @ArchTest
    static final ArchRule should_class_with_name_controller_should_be_in_adapter_primary_package 
= classes().that().haveSimpleNameContaining("Controller")
            .should().resideInAPackage("..adapter.primary..");

}

W powyższym fragmencie istnieją trzy ciekawe elementy. Pierwszym z nich jest @AnalyzeClasses. Ta anotacja określa jakie klasy, pakiety będą brane pod uwagę podczas analizy. Drugim jest @ArchTest, który określa nasze reguły. Trzecim elementem jest sposób zapisu zasady. Jest to statyczne finalne pole, które jest zainicjalizowany dsl. Nic nie stoi na przeszkodzie aby regułę napisać w formie metody testowej.

@ArchTest
    static void core_elements_should_not_depens_from_adapter(JavaClasses classes) {

        ArchRule rule  = noClasses().that().resideInAPackage("..core..")
.should().dependOnClassesThat().resideInAPackage("..adapter..");
        rule.check(classes);
    }

Dsl z którego korzysta archunit posiada wiele możliwości zapisu reguł. Łatwo mogę stworzyć reguły, które mówią że w jakimś pakiecie istnieją tylko interfejsy. Kolejnymi mogą być ich implementacje a także klasa musi posiadać specyficzną anotację.

    @ArchTest
    static final ArchRule secondary_port_should_be_interfaces = 
classes().that().resideInAPackage("..core.port.secondary..").should().beInterfaces();

    @ArchTest
    static final ArchRule primary_port_should_implement_interface_from_secondary =
            classes().that().resideInAnyPackage("..adapter.secondary..").and()
.haveSimpleNameContaining("Adapter")
                    .should().implement(resideInAnyPackage("..core.port.secondary.."));

    @ArchTest
    static final ArchRule controllers_should_be_annotated_with_annotation = classes().that()
.haveSimpleNameEndingWith("Controller").and().resideInAnyPackage(
            "..adapter.primary..").should().beAnnotatedWith(MyControllerAnnotation.class);

Z powyższych przykładów widać, że niektóre definicje reguł mogą się powtarzać. Dzięki archunit łatwo możesz poradzić sobie z takimi duplikatami.

 @ArchTest
    static final ArchRule methods_in_domain_services_should_not_be_public =
            methods().that().areDeclaredInClassesThat(areServicesInPrimaryPort())
.should().notBePublic();

    @ArchTest
    static final ArchRule fields_in_domain_services_should_private_and_final =
            fields().that().areDeclaredInClassesThat(areServicesInPrimaryPort())
.should().haveModifier(JavaModifier.FINAL).andShould().bePrivate();

    private static DescribedPredicate<JavaClass> areServicesInPrimaryPort() {
        return packageForCorePrimaryPort().and(classIsService());
    }

    private static DescribedPredicate<JavaClass> classIsService() {
        return JavaClass.Predicates.simpleNameEndingWith("Service");
    }

    private static DescribedPredicate<JavaClass> packageForCorePrimaryPort() {
        return JavaClass.Predicates.resideInAPackage("..core.port.primary..");
    }

The End

W powyższych przykładach pokazałem krótki wstęp do archunit. Dzięki bogatemu DSL łatwo możesz tworzyć swoje reguły dla systemu. Niektóre z nich mogą być proste a inne dużo bardziej skomplikowane. Najważniejszym punktem jest to, że Twoja architektura będzie się zmieniała w czasie ale wciąż każdy będzie znał jej zasady. W momencie gdy, ktoś te zasady złamie to czytelny sposób zostanie o tym poinformowany.

Kod dostępny na githubie.