JUnit 5 test engine for ArchUnit that enables running architecture tests as part of JUnit 5 test suites
npx @tessl/cli install tessl/maven-com-tngtech-archunit--archunit-junit5-engine@1.4.0ArchUnit JUnit 5 Engine provides seamless integration between ArchUnit architecture testing and JUnit 5 test suites. This engine enables automatic discovery and execution of ArchUnit architecture tests as part of the JUnit Platform, allowing architectural constraints to be validated during regular test execution with full IDE and build tool support.
testImplementation 'com.tngtech.archunit:archunit-junit5-engine:1.4.1'<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5-engine</artifactId>
<version>1.4.1</version>
<scope>test</scope>
</dependency>import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchIgnore;
import com.tngtech.archunit.junit.ArchTag;
import com.tngtech.archunit.junit.ArchTests;
import com.tngtech.archunit.junit.CacheMode;
import com.tngtech.archunit.junit.LocationProvider;
import com.tngtech.archunit.junit.engine_api.FieldSelector;
import com.tngtech.archunit.junit.engine_api.FieldSource;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.core.importer.Location;
import java.util.Set;
import java.lang.reflect.Field;import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
@AnalyzeClasses(packages = "com.myapp")
public class ArchitectureTest {
@ArchTest
public static final ArchRule layered_architecture =
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
@ArchTest
public void services_should_only_be_accessed_by_controllers(JavaClasses classes) {
classes().that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..")
.check(classes);
}
}The ArchUnit JUnit 5 Engine is built around several key components:
ArchUnitTestEngine registered as 'archunit' engine@AnalyzeClasses annotated classes containing @ArchTest fields/methodsProvides JUnit Platform discovery selectors for programmatic test selection beyond standard class/method selection.
/**
* Selector for ArchUnit test fields, enables programmatic discovery of specific @ArchTest fields
*/
public final class FieldSelector implements DiscoverySelector {
/**
* Select a field by class name and field name
* @param className - Fully qualified class name containing the field
* @param fieldName - Name of the @ArchTest field to select
* @return FieldSelector for the specified field
*/
public static FieldSelector selectField(String className, String fieldName);
/**
* Select a field by class and field name
* @param javaClass - Class containing the field
* @param fieldName - Name of the @ArchTest field to select
* @return FieldSelector for the specified field
*/
public static FieldSelector selectField(Class<?> javaClass, String fieldName);
/**
* Select a field by class and field instance
* @param javaClass - Class containing the field
* @param field - The @ArchTest field to select
* @return FieldSelector for the specified field
*/
public static FieldSelector selectField(Class<?> javaClass, Field field);
/** Get the Java class containing the selected field */
@Internal
public Class<?> getJavaClass();
/** Get the selected Java field */
@Internal
public Field getJavaField();
}Provides detailed source information for ArchUnit test fields enabling IDE navigation and reporting.
/**
* Test source implementation for ArchUnit test fields
*/
@PublicAPI(usage = ACCESS)
public final class FieldSource implements TestSource {
/**
* Create FieldSource from Field instance - internal use only
* @param field - The field to create source for
* @return FieldSource instance
*/
@Internal
static FieldSource from(Field field);
/** Get the fully qualified class name containing the field */
@PublicAPI(usage = ACCESS)
public String getClassName();
/** Get the Java class containing the field */
@PublicAPI(usage = ACCESS)
public Class<?> getJavaClass();
/** Get the field name */
@PublicAPI(usage = ACCESS)
public String getFieldName();
}The engine automatically registers with JUnit Platform through service provider interface.
Service Registration:
META-INF/services/org.junit.platform.engine.TestEnginecom.tngtech.archunit.junit.internal.ArchUnitTestEngine"archunit"Tests must use these annotations from the core ArchUnit library:
/**
* Specifies which packages/locations should be scanned when running JUnit 5 tests
*/
@Testable
@Target({TYPE})
@Retention(RUNTIME)
@PublicAPI(usage = ACCESS)
public @interface AnalyzeClasses {
/** Packages to look for within the classpath/modulepath */
String[] packages() default {};
/** Classes that specify packages to look for */
Class<?>[] packagesOf() default {};
/** Implementations of LocationProvider for custom class sources */
Class<? extends LocationProvider>[] locations() default {};
/** Whether to look for classes on the whole classpath */
boolean wholeClasspath() default false;
/** Types of ImportOption to use for filtering the class import */
Class<? extends ImportOption>[] importOptions() default {};
/** Controls if JavaClasses should be cached by location */
CacheMode cacheMode() default CacheMode.FOREVER;
}
/**
* Marks ArchUnit tests to be executed by the test infrastructure
*/
@Testable
@Target({FIELD, METHOD})
@Retention(RUNTIME)
public @interface ArchTest {
}
/**
* Marks rules to be ignored by the test support
*/
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface ArchIgnore {
/** Why the test is ignored */
String reason() default "";
}
/**
* Repeatable annotation for tagging ArchTest fields/methods/classes
*/
@Inherited
@Documented
@Retention(RUNTIME)
@PublicAPI(usage = ACCESS)
@Repeatable(ArchTags.class)
@Target({TYPE, METHOD, FIELD})
public @interface ArchTag {
/** The actual tag value - must adhere to JUnit Platform tag syntax rules */
String value();
}The engine integrates with core ArchUnit types and functionality:
/**
* Collections of ArchUnit test rules for inclusion in test classes
*/
@PublicAPI(usage = ACCESS)
public final class ArchTests {
/**
* @param definitionLocation The class whose @ArchTest members should be included in this test
* @return the ArchTests of the supplied class
*/
@PublicAPI(usage = ACCESS)
public static ArchTests in(Class<?> definitionLocation);
}
/**
* Allows custom implementation to supply Locations to be imported by the JUnit test infrastructure
*/
@PublicAPI(usage = INHERITANCE)
public interface LocationProvider {
/**
* Returns locations to be imported for the current test run
* @param testClass The class object of the test currently executed
* @return The locations to import
*/
Set<Location> get(Class<?> testClass);
}
/**
* Determines how the JUnit test support caches classes
*/
@PublicAPI(usage = ACCESS)
public enum CacheMode {
/** Cache imported Java classes for current test class only */
@PublicAPI(usage = ACCESS)
PER_CLASS,
/** Cache imported Java classes by location using SoftReferences */
@PublicAPI(usage = ACCESS)
FOREVER
}Field-based Tests (Recommended):
@AnalyzeClasses(packages = "com.example")
public class MyArchTest {
@ArchTest
public static final ArchRule rule = classes()
.that().resideInAPackage("..service..")
.should().notDependOn("..controller..");
}Method-based Tests:
@AnalyzeClasses(packages = "com.example")
public class MyArchTest {
@ArchTest
public void methodTest(JavaClasses classes) {
classes().that().resideInAPackage("..service..")
.should().notDependOn("..controller..")
.check(classes);
}
}Rule Libraries:
@AnalyzeClasses(packages = "com.example")
public class MyArchTest {
@ArchTest
public static final ArchTests rules = ArchTests.in(MyRuleLibrary.class);
}Property-based Filtering:
# In archunit.properties or system property
junit.testFilter=specificRuleName,anotherRuleJUnit Platform Filtering: Standard JUnit 5 filters work: class name filters, package filters, tags, etc.
The engine provides comprehensive error reporting:
@AnalyzeClasses annotations or invalid test method signaturesJavaClasses parameter)JavaClasses to avoid repeated expensive import operations// Key internal types (for understanding engine behavior)
interface CreatesChildren {
void createChildren(ElementResolver resolver);
}
// Test descriptor types extend JUnit Platform interfaces
abstract class AbstractArchUnitTestDescriptor extends AbstractTestDescriptor
implements Node<ArchUnitEngineExecutionContext>
class ArchUnitTestEngine extends HierarchicalTestEngine<ArchUnitEngineExecutionContext>
class ArchUnitEngineDescriptor extends EngineDescriptor
implements Node<ArchUnitEngineExecutionContext>