ArchUnit Verifies Architecture Rules for Java Applications

MMS Founder
MMS Johan Janssen

Article originally posted on InfoQ. Visit InfoQ

ArchUnit allows developers to enforce architecture rules such as naming conventions, class access to other classes, and the prevention of cycles. The library was originally created in 2017 by Peter Gafert and version 1.0.0 was released in October.

ArchUnit works with all Java test frameworks and offers specific dependencies for JUnit. The following dependency should be used for JUnit 5:


    com.tngtech.archunit
    archunit-junit5
    1.0.0
    test

Now the ClassFileImporter can be used to import Java bytecode into Java code. For example, to import all classes in the org.example package:

JavaClasses javaClasses = new ClassFileImporter().importPackages("org.example");

Now the ArchRule class may be used to define architectural rules for the imported Java classes in a Domain Specific Language (DSL). There are various types of checks available, the first one is for package dependencies. The check specifies that no classes inside repository packages should use classes inside controller packages:

ArchRule rule = noClasses()
    .that().resideInAPackage("..repository..")
    .should().dependOnClassesThat().resideInAPackage("..controller..");

Two classes are used to verify the rules, a CourseController class inside a controller package and a CourseRepository class inside a repository package:

public class CourseController {
	private CourseRepository courseRepository;
}

public class CourseRepository {
	CourseController courseController;
}

This is not allowed by the ArchRule defined before, which can be tested automatically with JUnit:

AssertionError assertionError =

    Assertions.assertThrows(AssertionError.class, () -> {
        rule.check(javaClasses);
});

String expectedMessage = """
	Architecture Violation [Priority: MEDIUM] - 
        Rule 'no classes that reside in a package 
        '..repository..' should depend on classes that reside in a package 
        '..controller..'' was violated (1 times):
	Field  has type 
         in (CourseRepository.java:0)""";

assertEquals(expectedMessage, assertionError.getMessage());

The CourseController and CourseRepository depend on each other, which often is a design flaw. The cycle check detects cycles between classes and packages:

ArchRule rule = slices()

    .matching("org.example.(*)..")
    .should().beFreeOfCycles();

AssertionError assertionError =     
    Assertions.assertThrows(AssertionError.class, () -> {
        rule.check(javaClasses);
});

String expectedMessage = """
	Architecture Violation [Priority: MEDIUM] - Rule 'slices matching 
        'org.example.(*)..' should be free of cycles' was violated (1 times):
	Cycle detected: Slice controller ->s
                	Slice repository ->s
                	Slice controller
  	1. Dependencies of Slice controller
    	- Field  has type 
             in (CourseController.java:0)
  	2. Dependencies of Slice repository
    	- Field  has type 
             in (CourseRepository.java:0)""";

assertEquals(expectedMessage, assertionError.getMessage());

Class and Package containment checks allow the verification of naming and location conventions. For example, to verify that no interfaces are placed inside implementation packages:

noClasses()
    .that().resideInAPackage("..implementation..")
    .should().beInterfaces().check(classes);

Or to verify that all interfaces have a name containing “Interface”:

noClasses()
    .that().areInterfaces()
    .should().haveSimpleNameContaining("Interface").check(classes);

These containment checks may be combined with an annotation check. For example, to verify that all classes in the controller package with a RestController annotation have a name ending with Controller:

classes()
    .that().resideInAPackage("..controller..")
    .and().areAnnotatedWith(RestController.class)
    .should().haveSimpleNameEndingWith("Controller");

Inheritance checks allow, for example, to verify that all classes implementing the Repository interface have a name ending with Repository:

classes().that().implement(Repository.class)
    .should().haveSimpleNameEndingWith("Repository")

With the layer checks, it’s possible to define the architecture layers of an application and then define the rules between the layers:

Architectures.LayeredArchitecture rule = layeredArchitecture()
    .consideringAllDependencies()
    // Define layers
    .layer("Controller").definedBy("..controller..")
    .layer("Repository").definedBy("..Repository..")
    // Add constraints
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Repository").mayOnlyBeAccessedByLayers("Controller");

AssertionError assertionError = 
    Assertions.assertThrows(AssertionError.class, () -> {
        rule.check(javaClasses);
});

String expectedMessage = """
	Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture 
        considering all dependencies, consisting of
	layer 'Controller' ('..controller..')
	layer 'Repository' ('..Repository..')
	where layer 'Controller' may not be accessed by any layer
	where layer 'Repository' may only be accessed by layers ['Controller']' 
        was violated (2 times):
	Field  has type 
         in (CourseRepository.java:0)
	Layer 'Repository' is empty""";

assertEquals(expectedMessage, assertionError.getMessage());

More information can be found in the extensive user guide and official examples from ArchUnit are available on GitHub.

About the Author

Subscribe for MMS Newsletter

By signing up, you will receive updates about our latest information.

  • This field is for validation purposes and should be left unchanged.