dependencies {
+ // testImplementation 'com.tngtech.archunit:archunit-junit5:1.5.0' // for JUnit 5
+ testImplementation 'com.tngtech.archunit:archunit-junit6:1.5.0'
+}
+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.
ArchUnit consists of the following production modules:
+archunit
archunit-junit4
archunit-junit5-api, archunit-junit5-engine, archunit-junit5-engine-api
archunit-junit6-api, archunit-junit6-engine, archunit-junit6-engine-api
The archunit-example is also relevant to end users.
These modules contain the infrastructure to integrate with JUnit 5 and contain the respective +
These modules contain the infrastructure to integrate with JUnit 5 & 6 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).
archunit-junit*-api contains the user API to write tests with ArchUnit’s JUnit support.
+archunit-junit*-engine contains the runtime engine to run those tests.
+archunit-junit*-engine-api contains API code for tools that want more detailed control
+over running ArchUnit JUnit tests, in particular a FieldSelector which can be used to
+instruct the ArchUnitTestEngine to run a specific rule field (compare JUnit Integration).
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 +which is illustrated for Maven and Gradle below. Alternatively, you can obtain the necessary JAR files directly from Maven Central.
To use ArchUnit in combination with JUnit 4, include the following dependency from -Maven Central:
+ArchUnit’s JUnit 5 & 6 artifacts follow 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. Just like JUnit Jupiter, ArchUnit offers one convenience
+artifact transitively including both API and engine with the correct scope, which in turn can be added
+as a test compile dependency. Thus, to include ArchUnit’s JUnit 5 & 6 support, simply add the following dependency
+from Maven Central:
dependencies {
+ // testImplementation 'com.tngtech.archunit:archunit-junit5:1.5.0' // for JUnit 5
+ testImplementation 'com.tngtech.archunit:archunit-junit6:1.5.0'
+}
+<dependency>
<groupId>com.tngtech.archunit</groupId>
- <artifactId>archunit-junit4</artifactId>
- <version>1.4.2</version>
+ <!-- <artifactId>archunit-junit5</artifactId> --> <!-- for JUnit 5 -->
+ <artifactId>archunit-junit6</artifactId>
+ <version>1.5.0</version>
<scope>test</scope>
</dependency>
To use ArchUnit in combination with JUnit 4, include the following dependency from +Maven Central:
+dependencies {
- testImplementation 'com.tngtech.archunit:archunit-junit4:1.4.2'
+dependencies {
+ testImplementation 'com.tngtech.archunit:archunit-junit4:1.5.0'
}
ArchUnit’s JUnit 5 artifacts follow 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. Just like JUnit Jupiter ArchUnit offers one convenience
-artifact transitively including both API and engine with the correct scope, which in turn can be added
-as a test compile dependency. Thus to include ArchUnit’s JUnit 5 support, simply add the following dependency
-from Maven Central:
<dependency>
<groupId>com.tngtech.archunit</groupId>
- <artifactId>archunit-junit5</artifactId>
- <version>1.4.2</version>
+ <artifactId>archunit-junit4</artifactId>
+ <version>1.5.0</version>
<scope>test</scope>
</dependency>
dependencies {
- testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
-}
-dependencies {
+ testImplementation 'com.tngtech.archunit:archunit:1.5.0'
+}
+<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
- <version>1.4.2</version>
+ <version>1.5.0</version>
<scope>test</scope>
</dependency>
dependencies {
- testImplementation 'com.tngtech.archunit:archunit:1.4.2'
-}
-At its core ArchUnit provides infrastructure to import Java bytecode into Java code structures.
-This can be done using the ClassFileImporter
At its core, ArchUnit provides infrastructure to import Java bytecode into Java code structures.
+This can be done using the ClassFileImporter:
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:
The two dots represent any number of packages (compare AspectJ Pointcuts); for details on
+the supported package pattern syntax see Package Identifiers. The returned object of
+type ArchRule can now be evaluated against a set of imported classes:
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 +for writing tests with JUnit. The main advantage is automatic caching of imported classes between tests (of the same imported classes), as well as reduction of boilerplate code.
@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
+// @RunWith(ArchUnitRunner.class) // Enable this line for JUnit 4
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class MyArchitectureTest {
@@ -824,7 +852,7 @@
-@RunWith(ArchUnitRunner::class) // Remove this line for JUnit 5!!
+// @RunWith(ArchUnitRunner::class) // Enable this line for JUnit 4
@AnalyzeClasses(packagesOf = [MyArchitectureTest::class])
class MyArchitectureTest {
@ArchTest
@@ -1315,6 +1343,187 @@
+6.2.2. Package Identifiers
+
+Several ArchUnit methods accept a String package identifier to describe a set of packages,
+for example resideInAPackage(..) and resideInAnyPackage(..) (and their negated variants)
+in the Lang API, slices().matching(..) and modules().definedByPackages(..) in the Library
+API, or the component stereotypes of a PlantUML component diagram. All of them delegate to
+the same underlying PackageMatcher, whose syntax is inspired by AspectJ type patterns and
+extended with capturing groups.
+
+
+Wildcards
+
+The syntax is built from the following elements:
+
+
+
+
+
+
+
+
+Pattern
+Meaning
+
+
+
+
+*
+Matches any non-empty sequence of characters not containing the dot ., i.e. exactly one package segment.
+
+
+..
+Matches any (possibly empty) sequence of characters that may contain the dot ., i.e. any number of package segments (including zero).
+
+
+(*)
+Like *, but additionally captures the matched segment, so it can be referenced later (e.g. as a slice identifier, see Slices).
+
+
+(**)
+Like .., but additionally captures the matched segments.
+
+
+[a|b]
+Alternation: matches either a or b. Alternations are only allowed inside brackets [...] or inside capturing groups (...).
+
+
+
+
+Note that (..) is not a valid capturing group — use (**) instead. () and [] cannot
+be nested inside each other.
+
+
+The segments matched by capturing groups can be retrieved from the result of
+PackageMatcher#match(String)
+via
+PackageMatcher.Result#getGroup(int)
+(groups are 1-based, in the order the capturing groups appear in the pattern).
+
+
+
+Worked Example
+
+Consider the following five classes and the packages they reside in:
+
+
+
+
+
+
+
+
+Class
+Package
+
+
+
+
+com.myapp.controller.SomeController
+com.myapp.controller
+
+
+com.myapp.service.SomeService
+com.myapp.service
+
+
+com.myapp.service.impl.SomeServiceImpl
+com.myapp.service.impl
+
+
+com.myapp.persistence.dao.SomeDao
+com.myapp.persistence.dao
+
+
+com.myapp.persistence.dao.jpa.SomeJpa
+com.myapp.persistence.dao.jpa
+
+
+
+
+The following table shows a range of package identifiers and which of the five packages
+above each of them matches:
+
+
+
+
+
+
+
+
+Package Identifier
+Matches
+
+
+
+
+com.myapp.service
+com.myapp.service only (exact match).
+
+
+com.myapp.*
+com.myapp.controller and com.myapp.service
+(exactly one more segment after com.myapp).
+
+
+com.myapp..
+All five packages — com.myapp.. matches com.myapp itself and every subpackage of it.
+
+
+..service
+com.myapp.service only (segment service at the end).
+
+
+..service..
+com.myapp.service and com.myapp.service.impl
+(any package containing a segment service).
+
+
+..dao..
+com.myapp.persistence.dao and com.myapp.persistence.dao.jpa.
+
+
+..impl
+com.myapp.service.impl only.
+
+
+com.myapp.(*)
+com.myapp.controller (group 1 = controller) and
+com.myapp.service (group 1 = service).
+The deeper packages do not match because (*) allows only a single segment.
+
+
+com.myapp.(*)..
+All five packages; group 1 captures the first sub-package below com.myapp
+(controller, service, service, persistence, persistence respectively).
+This is the typical pattern used by slices().matching(..).
+
+
+..[service|controller]..
+com.myapp.controller, com.myapp.service and com.myapp.service.impl.
+
+
+
+
+
+What is Matched Against the Pattern
+
+An important detail is that the package identifier is matched against the
+package name of a class, not against the fully qualified class name.
+So for com.myapp.service.SomeService the string that is checked against
+the pattern is com.myapp.service, not com.myapp.service.SomeService.
+
+
+This means that a pattern like ..SomeService will not match a class named SomeService,
+because SomeService is the simple class name, not a package segment. To match by class
+name use a name-based predicate such as
+haveSimpleName("SomeService") or haveNameMatching(".*SomeService") instead.
+
+
+
Most entrypoints in ArchRuleDefinition create ArchRules that evaluate to EvaluationResults with Priority.MEDIUM; ArchRuleDefinition.priority allows to create rules with other Priority, e.g.:
ArchRule rule = ArchRuleDefinition.priority(Priority.LOW).noClasses() // ...
+The resulting error messages show the priority accordingly:
+java.lang.AssertionError: Architecture Violation [Priority: LOW] - Rule 'no classes // ...
+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 @@ -1804,7 +2032,10 @@
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:
+infixes, and then write assertions against those slices. Thematching(..) argument follows
+the package pattern syntax described in Package Identifiers, where the parentheses
+(*) / (**) mark the captured segment(s) that are used as slice identifiers.
+At the moment this is for example:
As the example shows, it shares some concepts with the Slices API. For example definedByPackages(..)
-follows the same semantics as slices().matching(..).
+follows the same semantics as slices().matching(..), using Package Identifiers.
Also, the configuration options for cycle detection mentioned in the last section are shared by these APIs.
But, it also offers several powerful concepts beyond that API to express many different modularization scenarios.
Note that the modules() API can be adjusted in many ways to model custom requirements.
-For further details, please take a look at the examples provided
-here.
Components must have at least one (possible multiple) stereotype(s). Each stereotype in the diagram
-must be unique and represent a valid package identifier (e.g. <<..example..>> where .. represents
-an arbitrary number of packages; compare the core API)
<<..example..>> where .. represents an arbitrary number of packages; compare the core API)
Components may have an optional alias (e.g. [Some Component] <<..example..>> as myalias). The alias must be alphanumeric and must not be quoted.
At the moment ArchUnit offers extended support for writing tests with JUnit 4 and JUnit 5. +
At the moment, ArchUnit offers extended support for writing tests with JUnit 4, JUnit 5, and JUnit 6. This mainly tackles the problem of caching classes between test runs and to remove some boilerplate.
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(importedClasses) is bloat and error prone (i.e. it can be forgotten).
check(importedClasses) is bloat and error-prone (i.e. it can be forgotten).
Make sure you follow the installation instructions at Installation, in particular to include the correct dependency for the respective JUnit support.
@@ -2619,13 +2850,13 @@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:
Tests look and behave very similar between JUnit 4 and JUnit 5 & 6. 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 & 6
+pick up the respective TestEngine transparently. A test typically looks the following way:
@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
+// @RunWith(ArchUnitRunner.class) // Enable this line for JUnit 4
@AnalyzeClasses(packages = "com.myapp")
public class ArchitectureTest {
@@ -2676,7 +2907,20 @@
-As a third option, locations can be specified freely by implementing a LocationProvider:
+As an alternative to specifying packages, you can directly specify individual classes to be analyzed:
+
@AnalyzeClasses(classes = {String.class, Integer.class})
+This allows for more fine-grained control over which classes are imported for testing.
+You can combine the classes property with other properties like packages, packagesOf, etc.
+In this case, all specified classes and packages will be imported for analysis.
As another option, locations can be specified freely by implementing a LocationProvider:
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:
Furthermore, in order to choose specific classes beneath those locations, ImportOptions can be
+specified (compare The Core API). The following example imports the classpath, but only considers
+production code and only considers code that is directly supplied and does not come from JARs:
Note for users of JUnit 5: the annotation @Disabled has no effect here.
+
Note for users of JUnit 5 & 6: the annotation @Disabled has no effect here.
Instead, @ArchIgnore should be used.
Often a project might end up with different categories of rules, for example "service 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:
If you omit the property (or set it to false) the original rule names are used as display names.
If you omit the property (or set it to false), the original rule names are used as display names.
In particular, the ArchUnit importer distinguishes 6 types of import dependencies:
+In particular, the ArchUnit importer distinguishes the following types of import dependencies:
permitted subclasses (i.e. classes that are permitted to extend or implement a sealed class or interface)
+enclosing types (i.e. outer classes of nested classes)
import.dependencyResolutionProcess.maxIterationsForMemberTypes = 1
import.dependencyResolutionProcess.maxIterationsForAccessesToTypes = 1
import.dependencyResolutionProcess.maxIterationsForSupertypes = -1
+import.dependencyResolutionProcess.maxIterationsForPermittedSubclasses = -1
import.dependencyResolutionProcess.maxIterationsForEnclosingTypes = -1
import.dependencyResolutionProcess.maxIterationsForAnnotationTypes = -1
import.dependencyResolutionProcess.maxIterationsForGenericSignatureTypes = -1
diff --git a/gradle.properties b/gradle.properties
index e5de813a5..6e94569ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
org.gradle.parallel=true
archunit.group=com.tngtech.archunit
-archunit.version=1.5.0-SNAPSHOT
+archunit.version=1.6.0-SNAPSHOT
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED