1. Introduction

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure. ArchUnit’s main focus is to automatically test architecture and coding rules, using any plain Java unit testing framework.

1.1. Module Overview

ArchUnit consists of the following production modules: archunit, archunit-junit4 as well as archunit-junit5-api, archunit-junit5-engine and archunit-junit5-engine-api. Also relevant for end users is the archunit-example module.

1.1.1. Module archunit

This module contains the actual ArchUnit core infrastructure required to write architecture tests: The ClassFileImporter, the domain objects, as well as the rule syntax infrastructure.

1.1.2. Module archunit-junit4

This module contains the infrastructure to integrate with JUnit 4, in particular the ArchUnitRunner to cache imported classes.

1.1.3. Modules archunit-junit5-*

These modules contain the infrastructure to integrate with JUnit 5 and contain the respective infrastructure to cache imported classes between test runs. archunit-junit5-api contains the user API to write tests with ArchUnit’s JUnit 5 support, archunit-junit5-engine contains the runtime engine to run those tests. archunit-junit5-engine-api contains API code for tools that want more detailed control over running ArchUnit JUnit 5 tests, in particular a FieldSelector which can be used to instruct the ArchUnitTestEngine to run a specific rule field (compare JUnit 4 & 5 Support).

1.1.4. Module archunit-example

This module contains example architecture rules and sample code that violates these rules. Look here to get inspiration on how to set up rules for your project, or at ArchUnit-Examples for the last released version.

2. Installation

To use ArchUnit, it is sufficient to include the respective JAR files in the classpath. Most commonly, this is done by adding the dependency to your dependency management tool, which is illustrated for Maven and Gradle below. Alternatively you can obtain the necessary JAR files directly from Maven Central.

2.1. JUnit 4

To use ArchUnit in combination with JUnit 4, include the following dependency from Maven Central:

pom.xml
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>0.9.3</version>
    <scope>test</scope>
</dependency>
build.gradle
dependencies {
    testCompile 'com.tngtech.archunit:archunit-junit4:0.9.3'
}

2.2. JUnit 5

ArchUnit’s JUnit 5 artifacts follows the pattern of JUnit Jupiter. There is one artifact containing the API, i.e. the compile time dependencies to write tests. Then there is another artifact containing the actual TestEngine used at runtime. The dependencies can be obtained from Maven Central. A typical Maven configuration could look like this:

pom.xml
...
<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.0</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.2.0</version>
                </dependency>
                <dependency>
                    <groupId>com.tngtech.archunit</groupId>
                    <artifactId>archunit-junit5-engine</artifactId>
                    <version>0.9.3</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>
...
<dependencies>
    ...
    <dependency>
        <groupId>com.tngtech.archunit</groupId>
        <artifactId>archunit-junit5-api</artifactId>
        <version>0.9.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>
...
To run ArchUnit JUnit 5 tests within an IDE, it might unfortunately be necessary to add archunit-junit5-engine as a project dependency with scope test as well. Configuring archunit-junit5-engine as a dependency of the Surefire Plugin compensates Maven’s lack of a test runtime scope, but is not reliably understood by IDEs.

The configuration for Gradle is much simpler due to the existence of a test runtime scope:

build.gradle
dependencies {
    ...
    testCompile 'com.tngtech.archunit:archunit-junit5-api:0.9.3'
    testRuntime 'com.tngtech.archunit:archunit-junit5-engine:0.9.3'
}

2.3. Other Test Frameworks

ArchUnit works with any test framework that executes Java code. To use ArchUnit in such a context, include the core ArchUnit dependency from Maven Central:

pom.xml
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>0.9.3</version>
    <scope>test</scope>
</dependency>
build.gradle
dependencies {
   testCompile 'com.tngtech.archunit:archunit:0.9.3'
}

3. Getting Started

ArchUnit tests are written the same way as any Java unit test and can be written with any Java unit testing framework. To really understand the ideas behind ArchUnit, one should consult Ideas and Concepts. The following will outline a "technical" getting started.

3.1. Importing Classes

At its core ArchUnit provides infrastructure to import Java bytecode into Java code structures. This can be done using the ClassFileImporter

JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");

The ClassFileImporter offers many ways to import classes. Some ways depend on the current project’s classpath, like importPackages(..). However there are other ways that do not, for example:

JavaClasses classes = new ClassFileImporter().importPath("/some/path");

The returned object of type JavaClasses represents a collection of elements of type JavaClass, where JavaClass in turn represents a single imported class file. You can in fact access most properties of the imported class via the public API:

JavaClass clazz = classes.get(Object.class);
System.out.print(clazz.getSimpleName()); // returns 'Object'

3.2. Asserting (Architectural) Constraints

To express architectural rules, like 'Services should only be accessed by Controllers', ArchUnit offers an abstract DSL-like fluent API, which can in turn be evaluated against imported classes. To specify a rule, use the class ArchRuleDefinition as entry point:

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

// ...

ArchRule myRule = classes()
    .that().resideInAPackage("..service..")
    .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

The two dots represent any number of packages (compare AspectJ Pointcuts). The returned object of type ArchRule can now be evaluated against a set of imported classes:

myRule.check(classes);

Thus the complete example could look like

@Test
public void Services_should_only_be_accessed_by_Controllers() {
    JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");

    ArchRule myRule = classes()
        .that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

    myRule.check(classes);
}

3.3. Using JUnit 4 or JUnit 5

While ArchUnit can be used with any unit testing framework, it provides extended support for writing tests with JUnit 4 and JUnit 5. The main advantage is automatic caching of imported classes between tests (of the same imported classes), as well as reduction of boilerplate code.

To use the JUnit support, declare ArchUnit’s ArchUnitRunner (only JUnit 4), declare the classes to import via @AnalyzeClasses and add the respective rules as fields:

@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class MyArchitectureTest {

    @ArchTest
    public static final ArchRule myRule = classes()
        .that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

}

The JUnit test support will automatically import (or reuse) the specified classes and evaluate any rule annotated with @ArchTest against those classes.

For further information on how to use the JUnit support refer to JUnit Support.

3.4. Using JUnit support with Kotlin

Using the JUnit support with Kotlin is quite similar to Java:

@RunWith(ArchUnitRunner::class) // Remove this line for JUnit 5!!
@AnalyzeClasses(packagesOf = [MyArchitectureTest::class])
class MyArchitectureTest {
    @ArchTest
    val rule_as_field = ArchRuleDefinition.noClasses().should()...

    @ArchTest
    fun rule_as_method(classes: JavaClasses) {
        val rule = ArchRuleDefinition.noClasses().should()...
        rule.check(classes)
    }
}

4. What to Check

The following section illustrates some typical checks you could do with ArchUnit.

4.1. Package Dependency Checks

package deps no access
noClasses().that().resideInAPackage("..source..")
    .should().dependOnClassesThat().resideInAPackage("..foo..")
package deps only access
classes().that().resideInAPackage("..foo..")
    .should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")

4.2. Class Dependency Checks

class naming deps
classes().that().haveNameMatching(".*Bar")
    .should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar")

4.3. Class and Package Containment Checks

class package contain
classes().that().haveSimpleNameStartingWith("Foo")
    .should().resideInAPackage("com.foo")

4.4. Inheritance Checks

inheritance naming check
classes().that().implement(Connection.class)
    .should().haveSimpleNameEndingWith("Connection")
inheritance access check
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byAnyPackage("..persistence..")

4.5. Annotation Checks

inheritance annotation check
classes().that().areAssignableTo(EntityManager.class)
    .should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)

4.6. Layer Checks

layer check
layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

4.7. Cycle Checks

cycle check
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()

5. Ideas and Concepts

ArchUnit is divided into different layers, where the most important ones are the "Core" layer, the "Lang" layer and the "Library" layer. In short the Core layer deals with the basic infrastructure, i.e. how to import byte code into Java objects. The Lang layer contains the rule syntax to specify architecture rules in a succinct way. The Library layer contains more complex predefined rules, like a layered architecture with several layers. The following section will explain these layers in more detail.

5.1. Core

Much of ArchUnit’s core API resembles the Java Reflection API. There are classes like JavaMethod, JavaField, and more, and the public API consists of methods like getName(), getMethods(), getType() or getParameters(). Additionally ArchUnit extends this API for concepts needed to talk about dependencies between code, like JavaMethodCall, JavaConstructorCall or JavaFieldAccess. For example, it is possible to programmatically iterate over javaClass.getAccessesFromSelf() and react to the imported accesses between this Java class and other Java classes.

To import compiled Java class files, ArchUnit provides the ClassFileImporter, which can for example be used to import packages from the classpath:

JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");

For more information refer to The Core API.

5.2. Lang

The Core API is quite powerful and offers a lot of information about the static structure of a Java program. However, tests directly using the Core API lack expressiveness, in particular with respect to architectural rules.

For this reason ArchUnit provides the Lang API, which offers a powerful syntax to express rules in an abstract way. Most parts of the Lang API are composed as fluent APIs, i.e. an IDE can provide valuable suggestions on the possibilities the syntax offers.

An example for a specified architecture rule would be:

ArchRule rule =
    classes().that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

Once a rule is composed, imported Java classes can be checked against it:

JavaClasses classes = new ClassFileImporter().importPackage("com.myapp");
ArchRule rule = // define the rule
rule.check(classes);

The syntax ArchUnit provides is fully extensible and can thus be adjusted to almost any specific need. For further information, please refer to The Lang API.

5.3. Library

The Library API offers predefined complex rules for typical architectural goals. For example a succinct definition of a layered architecture via package definitions. Or rules to slice the code base in a certain way, for example in different areas of the domain, and enforce these slices to be acyclic or independent of each other. More detailed information is provided in The Library API.

6. The Core API

The Core API is itself divided into the domain objects and the actual import.

6.1. Import

As mentioned in Ideas and Concepts the backbone of the infrastructure is the ClassFileImporter, which provides various ways to import Java classes. One way is to import packages from the classpath, or the complete classpath via

JavaClasses classes = new ClassFileImporter().importClasspath();

However, the import process is completely independent of the classpath, so it would be well possible to import any path from the file system:

JavaClasses classes = new ClassFileImporter().importPath("/some/path/to/classes");

The ClassFileImporter offers several other methods to import classes, for example locations can be specified as URLs or as JAR files.

Furthermore specific locations can be filtered out, if they are contained in the source of classes, but should not be imported. A typical use case would be to ignore test classes, when the classpath is imported. This can be achieved by specifying ImportOptions:

ImportOption ignoreTests = new ImportOption() {
    @Override
    public boolean includes(Location location) {
        return !location.contains("/test/"); // ignore any URI to sources, that contains '/test/'
    }
};

JavaClasses classes = new ClassFileImporter().withImportOption(ignoreTests).importClasspath();

A Location is principally an URI, i.e. ArchUnit considers sources as File or JAR URIs

For the two common cases to skip importing JAR files and to skip importing test files (for typical setups, like a Maven or Gradle build), there already exist predefined ImportOptions:

new ClassFileImporter()
    .withImportOption(ImportOption.Predefined.DONT_INCLUDE_JARS)
    .withImportOption(ImportOption.Predefined.DONT_INCLUDE_TESTS)
    .importClasspath();

6.1.1. Dealing with Missing Classes

While importing the requested classes (e.g. target/classes or target/test-classes) it can happen, that a class within the scope of the import has a reference to a class outside of the scope of the import. This will naturally happen, if the classes of the JDK are not imported, since then for example any dependency on Object.class will be unresolved within the import.

At this point ArchUnit needs to decide how to treat these classes that are missing from the import. By default, ArchUnit creates a stub, i.e. a JavaClass that has all the known information, like the fully qualified name or the method called. However, this stub might naturally lack some information, like superclasses, annotations or other details that cannot be determined without importing the bytecode of this class.

Obviously in some cases this might not be the desired behavior, since for example rules that target superclasses or annotations might not behave as expected, if the information is missing from the import. Thus how the importer behaves when classes are missing can be freely configured, for example by telling ArchUnit to try to resolve those missing dependencies from the classpath and do a full import with the complete type hierarchy. To find out, how to configure this behavior, refer to Configuring the Resolution Behavior.

6.2. Domain

The domain objects represent Java code, thus the naming should be pretty straight forward. Most commonly, the ClassFileImporter imports instances of type JavaClass. A rough overview looks like this:

domain overview

Most objects resemble the Java Reflection API, including inheritance relations. Thus a JavaClass has JavaMembers, which can in turn be either JavaField, JavaMethod, JavaConstructor (or JavaStaticInitializer). While not present within the reflection API, it makes sense to introduce an expression for anything that can access other code, which ArchUnit calls 'code unit', and is in fact either a method, a constructor (including the class initializer) or a static initializer of a class (e.g. a static { …​ } block, a static field assignment, etc.).

Furthermore one of the most interesting features of ArchUnit, that exceeds the Java Reflection API, is the concept of accesses to another class. On the lowest level accesses can only take place from a code unit (as mentioned, any block of executable code) to either a field (JavaFieldAccess), a method (JavaMethodCall) or constructor (JavaConstructorCall).

ArchUnit imports the whole graph of classes and their relationship to each other. While checking the accesses from a class is pretty isolated (the bytecode offers all this information), checking accesses to a class requires the whole graph to be built first. To distinguish which sort of access is referred to, methods will always clearly state fromSelf and toSelf. For example, every JavaField allows to call JavaField#getAccessesToSelf(), to retrieve all code units within the graph, that access this specific field. The resolution process through inheritance is not completely straight forward. Consider for example

resolution example

The bytecode will record a field access from ClassAccessing.accessField() to ClassBeingAccessed.accessedField. However, there is no such field, since the field is actually declared in the superclass. This is the reason, that a JavaFieldAccess has no JavaField as its target, but a FieldAccessTarget. In other words, ArchUnit models the situation, as it is found within the bytecode, and an access target is not an actual member within another class. If a member is queried for accessesToSelf() though, ArchUnit will resolve the necessary targets and determine, which member is represented by which target. The situation looks roughly like

resolution overview

Two things might seem strange at the first look.

First, why can a target resolve to zero matching members? The reason is, that the set of classes that was imported does not need to have all classes involved within this resolution process. Consider the above example, if SuperClassBeingAccessed would not be imported, ArchUnit would have no way of knowing, where the actual targeted field resides. Thus in this case the resolution would return zero elements.

Second, why can there be more than one resolved methods for method calls? The reason for this is, that a call target might indeed match several methods in those cases, for example:

diamond example

While this situation will always be resolved in a specified way for a real program, ArchUnit can not do the same. Instead, the resolution will report all candidates that match a specific access target, so in the above example, the call target C.targetMethod() would in fact resolve to two JavaMethods, namely A.targetMethod() and B.targetMethod(). Likewise a check of either A.targetMethod.getCallsToSelf() or B.targetMethod.getCallsToSelf() would return the same call from D.callTargetMethod() to C.targetMethod().

6.2.1. Domain Objects, Reflection and the Classpath

ArchUnit tries to offer a lot of information from the bytecode, for example a JavaClass provides details like if it is an Enum or an Interface, modifiers like public or abstract, but also the source, where this class was imported from (namely the URI mentioned in the first section). However, if information if missing, and the classpath is correct, ArchUnit offers some convenience to rely on the reflection API for extended details. For this reason, most Java*-Objects offer a method reflect(), which will in fact try to resolve the respective object from the Reflection API. For example

JavaClasses classes = new ClassFileImporter().importClasspath(new ImportOptions());

// ArchUnit's java.lang.String
JavaClass javaClass = classes.get(String.class);
// Reflection API's java.lang.String
Class<?> stringClass = javaClass.reflect();

// ArchUnit's public int java.lang.String.length()
JavaMethod javaMethod = javaClass.getMethod("length");
// Reflection API's public int java.lang.String.length()
Method lengthMethod = javaMethod.reflect();

However, this will throw an Exception, if the respective classes are missing on the classpath (e.g. because they were just imported from some file path).

This restriction also applies to handling Annotations in a more convenient way. Consider some Annotation

@interface CustomAnnotation {
    String value();
}

If you need to access this annotation, without this annotation on the classpath you must rely on

JavaAnnotation annotation = javaClass.getAnnotationOfType("some.pkg.CustomAnnotation");
// result is untyped, since it might not be on the classpath (e.g. enums)
Object value = annotation.get("value");

So there is neither type safety nor automatic refactoring support. If this annotation is on the classpath, however, this can be written way more naturally:

CustomAnnotation annotation = javaClass.getAnnotationOfType(CustomAnnotation.class);
String value = annotation.value();

ArchUnit’s own rule APIs (compare The Lang API) never rely on the classpath though. Thus the evaluation of default rules and syntax combinations, described in the next section, does not depend on whether the classes were imported from the classpath or some JAR / folder.

7. The Lang API

7.1. Composing Rules

The Core API is pretty powerful with regard to all the details from the bytecode that it provides to tests. However, tests written this way lack conciseness and fail to convey the architectural concept that they should assert. Consider:

Set<JavaClass> services = new HashSet<>();
for (JavaClass clazz : classes) {
    // choose those classes with FQN with infix '.service.'
    if (clazz.getName().contains(".service.")) {
        services.add(clazz);
    }
}

for (JavaClass service : services) {
    for (JavaAccess<?> access : service.getAccessesFromSelf()) {
        String targetName = access.getTargetOwner().getName();

        // fail if the target FQN has the infix ".controller."
        if (targetName.contains(".controller.")) {
            String message = String.format(
                    "Service %s accesses Controller %s in line %d",
                    service.getName(), targetName, access.getLineNumber());
            Assert.fail(message);
        }
    }
}

What we want to express, is the rule "no classes that reside in a package 'service' should access classes that reside in a package 'controller'". Nevertheless, it’s hard to read through that code and distill that information. And the same process has to be done every time, someone needs to understand the semantics of this rule.

To solve this shortcoming, ArchUnit offers a high level API to express architectural concepts in a concise way. In fact, we can write code, that is almost equivalent to the prose rule text mentioned before:

ArchRule rule = ArchRuleDefinition.noClasses()
    .that().resideInAPackage("..service..")
    .should().accessClassesThat().resideInAPackage("..controller..");

rule.check(classes);

The only difference to colloquial language, are the ".." in the package notation, which refers to any number of packages. Thus "..service.." just expresses "any package that contains some sub-package 'service'", e.g. com.myapp.service.any. If this test fails, it will report an AssertionError with the following message:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] -
Rule 'no classes that reside in a package '..service..'
should access classes that reside in a package '..controller..'' was violated (1 times):
Method <some.pkg.service.SomeService.callController()>
calls method <some.pkg.controller.SomeController.execute()>
in (SomeService.java:14)

So as a benefit, the assertion error contains the full rule text out of the box and reports all violations including the exact class and line number. The rule API also allows to combine predicates and conditions:

noClasses()
    .that().resideInAPackage("..service..")
    .or().resideInAPackage("..persistence..")
    .should().accessClassesThat().resideInAPackage("..controller..")
    .orShould().accessClassesThat().resideInAPackage("..ui..")

rule.check(classes);

7.2. Creating Custom Rules

In fact, most architectural rules take the form

classes that ${PREDICATE} should ${CONDITION}

In other words, we always want to limit imported classes to a relevant subset, and then evaluate some condition to see that all those classes satisfy it. ArchUnit’s API allows you, to do just that, by exposing the concepts of DescribedPredicate and ArchCondition. So the rule above, is just an application of this generic API:

DescribedPredicate<JavaClass> resideInAPackageService = // define the predicate
ArchCondition<JavaClass> accessClassesThatResideInAPackageController = // define the condition

noClasses().that(resideInAPackageService)
    .should(accessClassesThatResideInAPackageController);

Thus, if the predefined API does not allow to express some concept, it is possible to extend it in any custom way, for example:

DescribedPredicate<JavaClass> haveAFieldAnnotatedWithPayload =
    new DescribedPredicate<JavaClass>("have a field annotated with @Payload"){
        @Override
        public boolean apply(JavaClass input) {
            boolean someFieldAnnotatedWithPayload = // iterate fields and check for @Payload
            return someFieldAnnotatedWithPayload;
        }
    };

ArchCondition<JavaClass> onlyBeAccessedBySecuredMethods =
    new ArchCondition<JavaClass>("only be accessed by @Secured methods") {
        @Override
        public void check(JavaClass item, ConditionEvents events) {
            for (JavaMethodCall call : item.getMethodCallsToSelf()) {
                if (!call.getOrigin().isAnnotatedWith(Secured.class)) {
                    String message = String.format(
                        "Method %s is not @Secured", call.getOrigin().getFullName());
                    events.add(SimpleConditionEvent.violated(call, message));
                }
            }
        }
    };

classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);

If the rule fails, the error message will be built from the supplied descriptions. In the example above, it would be

classes that have a field annotated with @Payload should only be accessed by @Secured methods

7.3. Predefined Predicates and Conditions

Often custom predicates and conditions like in the last section can be composed from predefined elements. ArchUnit’s basic convention for predicates is, that they are defined in an inner class Predicates within the type they target. For example, one can find the predicate to check for the simple name of a JavaClass as

JavaClass.Predicates.simpleName(String)

Predicates can be joined using the methods predicate.or(other) and predicate.and(other). So for example a predicate testing for a class with simple name "Foo" that is serializable could be created the following way:

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleName;

DescribedPredicate<JavaClass> serializableNamedFoo =
    simpleName("Foo").and(assignableTo(Serializable.class));

Note that for some properties, there exist interfaces with predicates defined for them. For example the property to have a name is represented by the interface HasName, consequently the predicate to check the name of a JavaClass, is the same as the predicate to check the name of a JavaMethod and resides within

HasName.Predicates.name(String)

This can at times lead to problems with the type system, if predicates are supposed to be joined. Since the or(..) method accepts a type of DescribedPredicate<? super T>, where T is the type of the first predicate. For example:

// Does not compile, because type(..) targets a subtype of HasName
HasName.Predicates.name("").and(JavaClass.Predicates.type(Serializable.class))

// Does compile, because name(..) targets a supertype of JavaClass
JavaClass.Predicates.type(Serializable.class).and(HasName.Predicates.name(""))

// Does compile, because the compiler now sees name(..) as a predicate for JavaClass
DescribedPredicate<JavaClass> name = HasName.Predicates.name("").forSubType();
name.and(JavaClass.Predicates.type(Serializable.class));

This behavior is somewhat tedious, but unfortunately it is a shortcoming of the Java type system, that cannot be circumvented in a satisfying way.

Just like predicates, there exist predefined conditions, that can be combined in a similar way. Since ArchCondition is a less generic concept, all predefined conditions can be found within ArchConditions:

ArchCondition<JavaClass> callEquals =
    ArchConditions.callMethod(Object.class, "equals", Object.class);
ArchCondition<JavaClass> callHashCode =
    ArchConditions.callMethod(Object.class, "hashCode");

ArchCondition<JavaClass> callEqualsOrHashCode = callEquals.or(callHashCode);

7.4. Rules with Custom Concepts

Earlier we stated, that most architectural rules take the form

classes that ${PREDICATE} should ${CONDITION}

However, we do not always talk about classes, if we express architectural concepts. We might have custom language, we might talk about modules, about slices, or on the other hand more detailed about fields, methods or constructors. A generic API will never be able to support every imaginable concept out of the box. Thus ArchUnit’s rule API has at its foundation a more generic API, that controls the types of objects that our concept targets.

import vs lang

To achieve this, any rule definition is based on a ClassesTransformer that defines, how JavaClasses are to be transformed to the desired rule input. In many cases, like the ones mentioned in the sections above, this is the identity transformation, passing classes on to the rule as they are. However, one can supply any custom transformation to express a rule about a different type of input object. For example:

ClassesTransformer<JavaField> fields = new AbstractClassesTransformer<JavaField>("fields") {
    @Override
    public Iterable<JavaField> doTransform(JavaClasses classes) {
        Set<JavaField> result = new HashSet<>();
        for (JavaClass javaClass : classes) {
            result.addAll(javaClass.getFields());
        }
        return result;
    }
};

all(fields).that(have(modifier(PUBLIC))).should(...)

Of course these transformers can represent any custom concept desired:

// how we map classes to business modules
ClassesTransformer<BusinessModule> businessModules = ...

// filter business module dealing with orders
DescribedPredicate<BusinessModule> dealWithOrders = ...

// check that the actual business module is independent of payment
ArchCondition<BusinessModule> beIndependentOfPayment = ...

all(businessModules).that(dealWithOrders).should(beIndependentOfPayment);

7.5. Controlling the Rule Text

If the rule is straight forward, the rule text that is created automatically should be sufficient in many cases. However, for rules that are not common knowledge, it is good practice to document the reason for this rule. This can be done the following way:

classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods)
    .because("@Secured methods will be intercepted, checking for increased priviledges " +
        "and obfuscating sensitive auditing information");

Nevertheless sometimes the generated rule text might not convey the real intention concisely enough (e.g. if multiple predicates or conditions are joined). In those cases it is possible, to completely override the rule text:

classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods)
    .as("Payload may only be accessed in a secure way");

7.6. Ignoring Violations

In legacy projects there might be too many violations to fix at once. Nevertheless, that code should be covered completely by architecture tests, to ensure that no further violations will be added to the existing code. One approach to ignore existing violations is to tailor the that(..) clause of the rules in question, to ignore certain violations. A more generic approach is, to ignore violations based on simple regex matches. For this one can put a file named archunit_ignore_patterns.txt in the root of the classpath. Every line will be interpreted as a regular expression and checked against reported violations. Violations with a message matching the pattern will be ignored. If no violations are left, the check will pass.

For example, suppose the class some.pkg.LegacyService violates a lot of different rules. It is possible to add

archunit_ignore_patterns.txt
.*some\.pkg\.LegacyService.*

All violations mentioning some.pkg.LegacyService will consequently be ignored, and rules that are only violated by such violations will report success instead of failure.

It is possible to add comments to ignore patterns by prefixing the line with a '#':

archunit_ignore_patterns.txt
# There are many known violations where LegacyService is involved; we'll ignore them all
.*some\.pkg\.LegacyService.*

8. The Library API

The Library API offers a growing collection of predefined rules, that offer a more concise API for more complex but common patterns, like a layered architecture or checks for cycles between slices (compare What to Check).

8.1. Architectures

The entrance point for checks of common architectural styles is

com.tngtech.archunit.library.Architectures

At the moment this only provides a convenient check for a layered architecture (compare What to Check), but in the future it might be extended for styles like a hexagonal architecture, pipes and filters, separation of business logic and technical infrastructure, etc.

8.2. Slices

Currently there are two "slice" rules offered by the Library API. These are basically rules that slice the code by packages, and contain assertions on those slices. The entrance point is

com.tngtech.archunit.library.dependencies.SlicesRuleDefinition

The API is based on the idea to sort classes into slices according to one or several package infixes, and then write assertions against those slices. At the moment this is for example:

// sort classes by the first package after 'myapp'
// then check those slices for cyclic dependencies
SlicesRuleDefinition.slices().matching("..myapp.(*)..").should().beFreeOfCycles()

// checks all subpackages of 'myapp' for cycles
SlicesRuleDefinition.slices().matching("..myapp.(**)").should().notDependOnEachOther()

// sort classes by packages between 'myapp' and 'service'
// then check those slices for not having any dependencies on each other
SlicesRuleDefinition.slices().matching("..myapp.(**).service..").should().notDependOnEachOther()

8.3. General Coding Rules

The Library API also offers a small set of coding rules that might be useful in various projects. Those can be found within

com.tngtech.archunit.library.GeneralCodingRules

These for example contain rules not to use java.util.logging, not to write to System.out (but use logging instead) or not to throw generic exceptions.

8.4. PlantUML Component Diagrams as rules

The Library API offers a feature that supports PlantUML diagrams. This feature is located in

com.tngtech.archunit.library.plantuml

ArchUnit can derive rules straight from PlantUML diagrams and check to make sure that all imported JavaClasses abide by the dependencies of the diagram. The respective rule can be created in the following way:

URL myDiagram = getClass().getResource("my-diagram.puml");

classes().should(adhereToPlantUmlDiagram(myDiagram, consideringAllDependencies()));

Diagrams supported have to be component diagrams and associate classes to components via stereotypes. The way this works is to use the respective package identifiers (compare ArchConditions.onlyHaveDependenciesInAnyPackage(..)) as stereotypes:

simple plantuml archrule example
@startuml
[Some Source] <<..some.source..>>
[Some Target] <<..some.target..>> as target

[Some Source] --> target
@enduml

Consider this diagram applied as a rule via adhereToPlantUmlDiagram(..), then for example a class some.target.Target accessing some.source.Source would be reported as a violation.

8.4.1. Configurations

There are different ways to deal with dependencies of imported classes not covered by the diagram at all. The behavior of the PlantUML API can be configured by supplying a respective Configuration:

// considers all dependencies possible (including java.lang, java.util, ...)
classes().should(adhereToPlantUmlDiagram(
        mydiagram, consideringAllDependencies())

// considers only dependencies specified in the PlantUML diagram
// (so any unknown depedency will be ignored)
classes().should(adhereToPlantUmlDiagram(
        mydiagram, consideringOnlyDependenciesInDiagram())

// considers only dependencies in any specified package
// (control the set of dependencies to consider, e.g. only com.myapp..)
classes().should(adhereToPlantUmlDiagram(
        mydiagram, consideringOnlyDependenciesInAnyPackage("..some.package.."))

It is possible to further customize which dependencies to ignore:

// there are further ignore flavors available
classes().should(adhereToPlantUmlDiagram(mydiagram).ignoreDependencies(predicate))

A PlantUML diagram used with ArchUnit must abide by a certain set of rules:

  1. Components must have a name

  2. Components must have at least one stereotype. Each stereotype in the diagram must be unique

  3. Components may have an optional alias

  4. Components must be defined before declaring dependencies

  5. Dependencies must use arrows only consisting of dashes, pointing right, e.g. -->

9. JUnit Support

At the moment ArchUnit offers extended support for writing tests with JUnit 4 and JUnit 5. This mainly tackles the problem of caching classes between test runs and to remove some boilerplate.

Consider a straight forward approach to write tests:

@Test
public void rule1() {
    JavaClasses classes = new ClassFileImporter().importClasspath();

    ArchRule rule = classes()...

    rule.check(classes);
}

@Test
public void rule2() {
    JavaClasses classes = new ClassFileImporter().importClasspath();

    ArchRule rule = classes()...

    rule.check(classes);
}

For bigger projects, this will have a significant performance impact, since the import can take a noticeable amount of time. Also rules will always be checked against the imported classes, thus the explicit call of check(classes) is bloat and error prone (i.e. it can be forgotten).

9.1. JUnit 4 & 5 Support

Make sure you follow the installation instructions at Installation, in particular to include the correct dependency for the respective JUnit support.

9.1.1. Writing tests

Tests look and behave very similar between JUnit 4 and 5. The only difference is, that with JUnit 4 it is necessary to add a specific Runner to take care of caching and checking rules, while JUnit 5 picks up the respective TestEngine transparently. A test typically looks the following way:

@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
@AnalyzeClasses(packages = "com.myapp")
public class ArchitectureTest {

    // ArchRules can just be declared as static fields and will be evaluated
    @ArchTest
    public static final ArchRule rule1 = classes().should()...

    @ArchTest
    public static final ArchRule rule2 = classes().should()...

    @ArchTest
    public static void rule3(JavaClasses classes) {
        // The runner also understands static methods with a single JavaClasses argument
        // reusing the cached classes
    }

}

The JavaClass cache will work in two ways. On the one hand it will cache the classes by test, so they can be reused by several rules declared within the same class. On the other hand, it will cache the classes by location, so a second test, that wants to import classes from the same URLs will reuse the classes previously imported as well. Note that this second caching uses soft references, so the classes will be dropped from memory, if the heap runs low. For further information see Controlling the Cache.

9.1.2. Controlling the Import

Which classes will be imported can be controlled in a declarative way through @AnalyzeClasses. If no packages or locations are provided, the whole classpath will be imported. You can specify packages to import as strings:

@AnalyzeClasses(packages = {"com.myapp.subone", "com.myapp.subone"})

To better support refactorings, packages can also be declared relative to classes, i.e. the packages these classes reside in will be imported:

@AnalyzeClasses(packagesOf = {SubOneConfiguration.class, SubTwoConfiguration.class})

As a third option, locations can be specified freely by implementing a LocationProvider:

public class MyLocationProvider implements LocationProvider {
    @Override
    public Set<Location> get(Class<?> testClass) {
        // Determine Locations (= URLs) to import
        // Can also consider the actual test class, e.g. to read some custom annotation
    }
}

@AnalyzeClasses(locations = MyLocationProvider.class)

Furthermore to choose specific classes beneath those locations, ImportOptions can be specified (compare The Core API). For example, to import the classpath, but only consider production code, and only consider code that is directly supplied and does not come from JARs:

@AnalyzeClasses(importOptions = {DontIncludeTests.class, DontIncludeJars.class})

As explained in The Core API, you can write your own custom implementation of ImportOption and then supply the type to @AnalyzeClasses.

9.1.3. Controlling the Cache

By default all classes will be cached by location. This means that between different test class runs imported Java classes will be reused, if the exact combination of locations has already been imported.

If the heap runs low, and thus the garbage collector has to do a big sweep in one run, this can cause a noticeable delay. On the other hand, if it is known, that no other test class will reuse the imported Java classes, it would make sense to deactivate this cache.

This can be achieved by configuring CacheMode.PER_CLASS, e.g.

@AnalyzeClasses(packages = "com.myapp.special", cacheMode = CacheMode.PER_CLASS)

The Java classes imported during this test run will not be cached by location and just be reused within the same test class. After all tests of this class have been run, the imported Java classes will simply be dropped.

9.1.4. Ignoring Tests

It is possible to skip tests by annotating them with @ArchIgnore, for example:

public class ArchitectureTest {

    // will run
    @ArchTest
    public static final ArchRule rule1 = classes().should()...

    // won't run
    @ArchIgnore
    @ArchTest
    public static final ArchRule rule2 = classes().should()...
}

9.1.5. Grouping Rules

Often a project might end up with different categories of rules, for example "service rules" and "persistence rules". It is possible to write one class for each set of rules, and then refer to those sets from another test:

public class ServiceRules {
    @ArchTest
    public static final ArchRule ruleOne = ...

    // further rules
}

public class PersistenceRules {
    @ArchTest
    public static final ArchRule ruleOne = ...

    // further rules
}

@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
@AnalyzeClasses
public class ArchitectureTest {

    @ArchTest
    public static final ArchRules serviceRules = ArchRules.in(ServiceRules.class);

    @ArchTest
    public static final ArchRules persistenceRules = ArchRules.in(PersistenceRules.class);

}

The runner will evaluate all rules within ServiceRules and PersistenceRules against the classes declared at ArchitectureTest. This also allows an easy reuse of a rule library in different projects or modules.

10. Advanced Configuration

Some behavior of ArchUnit can be centrally configured by adding a file archunit.properties to the root of the classpath (e.g. under src/test/resources). This section will outline those configuration options.

10.1. Configuring the Resolution Behavior

As mentioned in Dealing with Missing Classes, it might be necessary to configure a different behavior, when referenced classes are missing from the import. One way that can be chosen out of the box is to resolve those classes from the classpath:

archunit.properties
resolveMissingDependenciesFromClassPath=true

If this resolves too many classes from the classpath (which can have a performance impact), it is possible, to configure only specific packages to be resolved from the classpath:

archunit.properties
classResolver=com.tngtech.archunit.core.importer.resolvers.SelectedClassResolverFromClasspath
classResolver.args=some.pkg.one,some.pkg.two

This configuration would only resolve the packages some.pkg.one and some.pkg.two from the classpath, and stub all other missing classes.

The last example also demonstrates, how the behavior can be customized freely, for example if classes are imported from a different source and are not on the classpath:

First Supply a custom implementation of

com.tngtech.archunit.core.importer.resolvers.ClassResolver

Then configure it

archunit.properties
classResolver=some.pkg.MyCustomClassResolver

If the resolver needs some further arguments, create a public constructor with one List<String> argument, and supply the concrete arguments as

archunit.properties
classResolver.args=myArgOne,myArgTwo

For further details, compare the sources of SelectedClassResolverFromClasspath.

10.2. MD5 Sums of Classes

Sometimes it can be valuable to record the MD5 sums of classes being imported, to track unexpected behavior. Since this has a performance impact, it is disabled by default, but it can be activated the following way:

archunit.properties
enableMd5InClassSources=true

If this feature is enabled, the MD5 sum can be queried as

javaClass.getSource().get().getMd5sum()