Was ist das genau und braucht man es?

Was ist ArchUnit?

ArchUnit ist ein Framework, um die Architektur des Projekts zu testen bzw. sicherzustellen, dass bestimmte Annahmen auch eingehalten werden.

Was kann zum Beispiel getestet werden?

  • Klassen innerhalb eines Package enthalten bestimmte Namen
  • Klassen werden nur aus einem bestimmten Layer aufgerufen
  • Es gibt keine zyklischen Abhängigkeiten

Wann verwenden?

Je früher ArchUnit in einem Projekt verwendet wird, desto eher lassen sich Probleme vermeiden. Wenn es schon zu Problemen gekommen ist, müssen diese erst beseitigt werden, bevor die Tests erfolgreich durchlaufen.

Das Laden aller Testklassen dauert je nach Projektgröße zum Teil mehrere Minuten. Daher sollten die Tests nur auf dem Buildserver z.B. nachts durchlaufen werden. Dafür kann man die Tets taggen mit @ArchTag und z.B. für Gradle die Tests wie folgt konfigurieren:

task unitTest(type: Test) { Test task ->
    task.useJUnitPlatform { JUnitPlatformOptions options ->
        options.excludeTags 'architecture', 'apiDoc'
        filter {
            includeTestsMatching "packagename*"
        }
    }
}

task architectureTest(type: Test) { Test task ->
    task.useJUnitPlatform { JUnitPlatformOptions options ->
        options.includeTags 'architecture'
    }
    task.mustRunAfter tasks.test
}

Gradle

    testCompile(
            "com.tngtech.archunit:archunit:${archunitVersion}",
            "com.tngtech.archunit:archunit-junit5-api:${archunitVersion}"
    )
    testRuntime "com.tngtech.archunit:archunit-junit5-engine:${archunitVersion}"

Beispiele

Um die Tests auszuführen, müssen zuerst alle Klassen geladen werden. Dies kann ein wenig dauern.

@ArchTag("architecture")
@AnalyzeClasses(packages = ArchitectureTest.IMPORT_PACKAGE, importOptions = { ExcludeTestClasses.class })
class ArchitectureTest {

    static final String IMPORT_PACKAGE = "packagename";

    private ArchitectureTest() {
    }
    
    // jetzt folgen die einzelnen Tests
    
}

Mit ExcludeTestClasses.java:

package packagename.quality;

import java.util.Set;
import java.util.regex.Pattern;

import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.core.importer.Location;

public class ExcludeTestClasses implements ImportOption {

    private static final Pattern MAVEN_PATTERN = Pattern.compile(".*/target/test-classes/.*");
    private static final Pattern GRADLE_PATTERN = Pattern.compile(".*/build/classes/([^/]+/)?test/.*");
    private static final Pattern INTELLIJ_PATTERN = Pattern.compile(".*/out/test/classes/.*");
    private static final Pattern ECLIPSE_PATTERN = Pattern.compile(".*/bin/test/.*");

    private static final Set<Pattern> EXCLUDED_PATTERN = ImmutableSet.of(MAVEN_PATTERN, GRADLE_PATTERN, INTELLIJ_PATTERN, ECLIPSE_PATTERN);

    @Override
    public boolean includes(Location location) {
        for (Pattern pattern : EXCLUDED_PATTERN) {
            if (location.matches(pattern)) {
                return false;
            }
        }
        return true;
    }
}

Test, ob Klassennamen richtig verwendet wurden:

    @ArchTest
    void klassenMitDemNamenControllerBefindenSichImPackageController(JavaClasses importedClasses) {
        ArchRule rule = classes()
                .that().haveNameMatching(".*Controller")
                .should().resideInAPackage("..controller..")
                .as("Klassen mit dem Namen Controller befinden sich im Package 'controller'");

        rule.check(importedClasses);
    }

Test, ob Klassen nicht aus einem anderen Layer aufgerufen wurden:

    @ArchTest
    void layerCheck(JavaClasses importedClasses) {
        ArchRule rule = layeredArchitecture()
            .layer("controller").definedBy("..controller..")
            .layer("service").definedBy("..service..")
            .layer("integration").definedBy("..integration..")

            .whereLayer("controller").mayNotBeAccessedByAnyLayer()
            .whereLayer("service").mayOnlyBeAccessedByLayers("controller", "service", "integration")
            .whereLayer("integration").mayNotBeAccessedByAnyLayer()

            .as("Zugriff auf Layer");

        rule.check(importedClasses);
    }

Test auf zyklische Abhängigkeiten:

    @ArchTest
    void cycleCheck(JavaClasses importedClasses) {
        ArchRule rule = slices().matching(IMPORT_PACKAGE + ".(*)..")
                .should().beFreeOfCycles()
                .as("Es gibt keine zyklischen Abhängigkeiten");

        rule.check(importedClasses);
    }

Test ignorieren:

    @ArchIgnore(reason = "Warum wird der Test temporär ignoriert?")