diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java
index a93b15be9f..e77a1e3e44 100644
--- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java
+++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java
@@ -38,6 +38,16 @@
* a rule checking for no accesses to classes assignable to C will not fail, since ArchUnit does not know about the details
* of class B, but only simple information like the fully qualified name. For information how to configure the import and
* resolution behavior of missing classes, compare {@link ClassFileImporter}.
+ *
+ * Classes to be analyzed can be specified in different ways:
+ *
+ * - {@link #packages()} - specify package names as strings
+ * - {@link #packagesOf()} - specify packages relative to classes
+ * - {@link #classes()} - specify individual classes to analyze
+ * - {@link #locations()} - specify custom locations via {@link LocationProvider}
+ * - {@link #wholeClasspath()} - import all classes on the classpath
+ *
+ * These options can be combined. If no option is specified, the package of the annotated test class will be imported.
*
* @see ArchUnitRunner
* @see ClassFileImporter
@@ -83,4 +93,9 @@
* @return The {@link CacheMode} to use for this test class.
*/
CacheMode cacheMode() default CacheMode.FOREVER;
+
+ /**
+ * @return Classes to be used for testing instead of packages
+ */
+ Class>[] classes() default {};
}
diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java
index 22198ce123..3547958469 100644
--- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java
+++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java
@@ -209,5 +209,10 @@ public CacheMode getCacheMode() {
public boolean scanWholeClasspath() {
return analyzeClasses.wholeClasspath();
}
+
+ @Override
+ public Class>[] getClassesToAnalyze() {
+ return analyzeClasses.classes();
+ }
}
}
diff --git a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java
index f96fae9e07..6ee100781a 100644
--- a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java
+++ b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java
@@ -41,6 +41,16 @@
* a rule checking for no accesses to classes assignable to C will not fail, since ArchUnit does not know about the details
* of class B, but only simple information like the fully qualified name. For information how to configure the import and
* resolution behavior of missing classes, compare {@link ClassFileImporter}.
+ *
+ * Classes to be analyzed can be specified in different ways:
+ *
+ * - {@link #packages()} - specify package names as strings
+ * - {@link #packagesOf()} - specify packages relative to classes
+ * - {@link #classes()} - specify individual classes to analyze
+ * - {@link #locations()} - specify custom locations via {@link LocationProvider}
+ * - {@link #wholeClasspath()} - import all classes on the classpath
+ *
+ * These options can be combined. If no option is specified, the package of the annotated test class will be imported.
*
* @see ClassFileImporter
*/
@@ -87,4 +97,9 @@
* @return The {@link CacheMode} to use for this test class.
*/
CacheMode cacheMode() default CacheMode.FOREVER;
+
+ /**
+ * @return Classes to be used for testing instead of packages
+ */
+ Class>[] classes() default {};
}
diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java
index b8f83d3174..abed0941d2 100644
--- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java
+++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java
@@ -324,6 +324,11 @@ public CacheMode getCacheMode() {
public boolean scanWholeClasspath() {
return analyzeClasses.wholeClasspath();
}
+
+ @Override
+ public Class>[] getClassesToAnalyze() {
+ return analyzeClasses.classes();
+ }
}
private static class TestMember {
diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java
index 449c2f3269..c108c69ece 100644
--- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java
+++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java
@@ -20,6 +20,7 @@
import com.tngtech.archunit.junit.engine_api.FieldSelector;
import com.tngtech.archunit.junit.engine_api.FieldSource;
import com.tngtech.archunit.junit.internal.ArchUnitTestEngine.SharedCache;
+import com.tngtech.archunit.junit.internal.testexamples.AnalyzeClassesWithClassesProperty;
import com.tngtech.archunit.junit.internal.testexamples.ClassWithPrivateTests;
import com.tngtech.archunit.junit.internal.testexamples.ComplexMetaTags;
import com.tngtech.archunit.junit.internal.testexamples.ComplexRuleLibrary;
@@ -1079,6 +1080,7 @@ void passes_AnalyzeClasses_to_cache() {
assertThat(request.getLocationProviders()).isEqualTo(expected.locations());
assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue();
assertThat(request.getImportOptions()).isEqualTo(expected.importOptions());
+ assertThat(request.getClassesToAnalyze()).isEmpty();
}
@Test
@@ -1104,6 +1106,21 @@ void a_class_with_analyze_classes_as_meta_annotation() {
assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue();
assertThat(request.getImportOptions()).isEqualTo(expected.importOptions());
}
+
+ @Test
+ void passes_AnalyzeClasses_with_new_classes_property_to_cache() {
+ execute(createEngineId(), AnalyzeClassesWithClassesProperty.class);
+
+ verify(classCache).getClassesToAnalyzeFor(eq(AnalyzeClassesWithClassesProperty.class), classAnalysisRequestCaptor.capture());
+ ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue();
+ AnalyzeClasses expected = AnalyzeClassesWithClassesProperty.class.getAnnotation(AnalyzeClasses.class);
+ assertThat(request.getClassesToAnalyze()).isEqualTo(expected.classes());
+ assertThat(request.getImportOptions()).isEqualTo(expected.importOptions());
+ assertThat(request.getPackageNames()).isEmpty();
+ assertThat(request.getPackageRoots()).isEmpty();
+ assertThat(request.getLocationProviders()).isEmpty();
+ assertThat(request.scanWholeClasspath()).as("scan whole classpath").isFalse();
+ }
}
@Nested
diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java
new file mode 100644
index 0000000000..52b4413ebf
--- /dev/null
+++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/AnalyzeClassesWithClassesProperty.java
@@ -0,0 +1,26 @@
+package com.tngtech.archunit.junit.internal.testexamples;
+
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.importer.ImportOption;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.library.testclasses.coveringallclasses.first.First;
+import com.tngtech.archunit.library.testclasses.coveringallclasses.second.Second;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
+
+@AnalyzeClasses(
+ classes = {First.class, Second.class},
+ importOptions = {ImportOption.DoNotIncludeTests.class, ImportOption.DoNotIncludeJars.class}
+)
+public class AnalyzeClassesWithClassesProperty {
+ @ArchTest
+ public static final ArchRule irrelevant = classes().should(new ArchCondition("exist") {
+ @Override
+ public void check(JavaClass item, ConditionEvents events) {
+ }
+ });
+}
diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java
index 52675d7f22..bbd650d876 100644
--- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java
+++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassAnalysisRequest.java
@@ -19,4 +19,6 @@ interface ClassAnalysisRequest {
CacheMode getCacheMode();
boolean scanWholeClasspath();
+
+ Class>[] getClassesToAnalyze();
}
diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java
index 44e9535f83..5102f114a9 100644
--- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java
+++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java
@@ -183,6 +183,7 @@ private Specific(ClassAnalysisRequest classAnalysisRequest, Class> testClass)
declaredLocations = ImmutableSet.builder()
.addAll(getLocationsOfPackages(classAnalysisRequest))
.addAll(getLocationsOfProviders(classAnalysisRequest, testClass))
+ .addAll(getLocationsOfClasses(classAnalysisRequest))
.addAll(classAnalysisRequest.scanWholeClasspath() ? Locations.inClassPath() : emptySet())
.build();
}
@@ -201,6 +202,12 @@ private Set getLocationsOfProviders(ClassAnalysisRequest classAnalysis
.collect(toSet());
}
+ private Set getLocationsOfClasses(ClassAnalysisRequest classAnalysisRequest) {
+ return stream(classAnalysisRequest.getClassesToAnalyze())
+ .flatMap(clazz -> Locations.ofClass(clazz).stream())
+ .collect(toSet());
+ }
+
private LocationProvider tryCreate(Class extends LocationProvider> providerClass) {
try {
return newInstanceOf(providerClass);
@@ -240,6 +247,7 @@ private static boolean noSpecificLocationRequested(ClassAnalysisRequest classAna
return classAnalysisRequest.getPackageNames().length == 0
&& classAnalysisRequest.getPackageRoots().length == 0
&& classAnalysisRequest.getLocationProviders().length == 0
+ && classAnalysisRequest.getClassesToAnalyze().length == 0
&& !classAnalysisRequest.scanWholeClasspath();
}
}
diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java
index eb1cb5b6e5..2ec7c2dd00 100644
--- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java
+++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java
@@ -108,6 +108,15 @@ public void gets_all_classes_relative_to_class() {
assertThat(classes.contain(getClass())).as("root class is contained itself").isTrue();
}
+ @Test
+ public void gets_all_classes_specified() {
+ JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, new TestAnalysisRequest()
+ .withClassesToAnalyze(getClass()));
+
+ assertThat(classes).hasSize(1);
+ assertThat(classes.contain(getClass())).as("root class is contained itself").isTrue();
+ }
+
@Test
public void get_all_classes_by_LocationProvider() {
JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, new TestAnalysisRequest()
diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java
index b395c989c1..2b5618d7e4 100644
--- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java
+++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/TestAnalysisRequest.java
@@ -12,6 +12,7 @@ class TestAnalysisRequest implements ClassAnalysisRequest {
private boolean wholeClasspath = false;
private Class extends ImportOption>[] importOptions = new Class[0];
private CacheMode cacheMode = CacheMode.FOREVER;
+ private Class>[] classesToAnalyze = new Class[0];
@Override
public String[] getPackageNames() {
@@ -33,6 +34,9 @@ public boolean scanWholeClasspath() {
return wholeClasspath;
}
+ @Override
+ public Class>[] getClassesToAnalyze() { return classesToAnalyze; }
+
@Override
public Class extends ImportOption>[] getImportOptions() {
return importOptions;
@@ -74,4 +78,9 @@ TestAnalysisRequest withCacheMode(CacheMode cacheMode) {
this.cacheMode = cacheMode;
return this;
}
+
+ TestAnalysisRequest withClassesToAnalyze(Class>... classesToAnalyze) {
+ this.classesToAnalyze = classesToAnalyze;
+ return this;
+ }
}
diff --git a/docs/userguide/009_JUnit_Support.adoc b/docs/userguide/009_JUnit_Support.adoc
index 89b7cfd0e0..dd7be23336 100644
--- a/docs/userguide/009_JUnit_Support.adoc
+++ b/docs/userguide/009_JUnit_Support.adoc
@@ -89,7 +89,18 @@ 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`:
+As an alternative to specifying packages, you can directly specify individual classes to be analyzed:
+
+[source,java,options="nowrap"]
+----
+@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`:
[source,java,options="nowrap"]
----