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 e4a0118ad6..cdffad88b0 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,18 @@
* 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, whereby the final result is the union of all classes derived from each option,
+ * i.e. each option just adds more classes to be analyzed. If no option is specified, the package of the annotated
+ * test class will be imported.
*
* @see ArchUnitRunner
* @see ClassFileImporter
@@ -83,4 +95,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 3f3ecba033..815e4937d8 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 e26f61f50b..57dba13a4d 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,18 @@
* 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, whereby the final result is the union of all classes derived from each option,
+ * i.e. each option just adds more classes to be analyzed. If no option is specified, the package of the annotated
+ * test class will be imported.
*
* @see ClassFileImporter
*/
@@ -87,4 +99,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 8046ada193..b5d857b689 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..b31b07aad5 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_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()).isEqualTo(expected.packages());
+ assertThat(request.getPackageRoots()).isEqualTo(expected.packagesOf());
+ assertThat(request.getLocationProviders()).isEqualTo(expected.locations());
+ 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 77ac8fd184..c87345a9ba 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
@@ -97,10 +97,38 @@ public void filters_jars_relative_to_class() {
@Test
public void gets_all_classes_relative_to_class() {
- JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, analyzePackagesOf(getClass()));
+ JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, analyzePackagesOf(this.getClass()));
assertThat(classes).isNotEmpty();
- assertThat(classes.contain(getClass())).as("root class is contained itself").isTrue();
+ assertThat(classes.contain(this.getClass())).as("root class is contained itself").isTrue();
+ }
+
+ @Test
+ public void gets_all_classes_specified() {
+ JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, new TestAnalysisRequest()
+ .withClassesToAnalyze(this.getClass()));
+
+ assertThat(classes).hasSize(1);
+ assertThat(classes.contain(this.getClass())).as("root class is contained itself").isTrue();
+ }
+
+ @Test
+ public void combines_locations_from_all_sources_into_union() {
+ doReturn(new ClassFileImporter().importClasses())
+ .when(cacheClassFileImporter).importClasses(anySet(), anyCollection());
+
+ cache.getClassesToAnalyzeFor(TestClass.class, new TestAnalysisRequest()
+ .withPackages("com.tngtech.archunit.junit.internal")
+ .withPackagesRoots(Rule.class)
+ .withLocationProviders(TestLocationProviderOfClass_String.class)
+ .withClassesToAnalyze(this.getClass()));
+
+ verify(cacheClassFileImporter).importClasses(anySet(), locationCaptor.capture());
+ assertThat(locationCaptor.getValue())
+ .has(locationContaining("archunit/junit/internal"))
+ .has(locationContaining("org/junit"))
+ .has(locationContaining("java/lang/String"))
+ .has(locationContaining("ClassCacheTest"));
}
@Test
@@ -109,13 +137,13 @@ public void get_all_classes_by_LocationProvider() {
.withPackagesRoots(ClassCacheTest.class)
.withLocationProviders(TestLocationProviderOfClass_String.class, TestLocationProviderOfClass_Rule.class));
- assertThatTypes(classes).contain(String.class, Rule.class, getClass());
+ assertThatTypes(classes).contain(String.class, Rule.class, this.getClass());
classes = cache.getClassesToAnalyzeFor(TestClassWithLocationProviderUsingTestClass.class,
analyzeLocation(LocationOfClass.Provider.class));
assertThatTypes(classes).contain(String.class);
- assertThatTypes(classes).doNotContain(getClass());
+ assertThatTypes(classes).doNotContain(this.getClass());
}
@Test
@@ -135,14 +163,14 @@ public void if_no_import_locations_are_specified_and_whole_classpath_is_set_fals
JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, defaultOptions);
- assertThatTypes(classes).contain(getClass(), TestAnalysisRequest.class);
+ assertThatTypes(classes).contain(this.getClass(), TestAnalysisRequest.class);
assertThatTypes(classes).doNotContain(ClassFileImporter.class);
}
@Test
public void if_whole_classpath_is_set_true_then_the_whole_classpath_is_imported() {
TestAnalysisRequest defaultOptions = new TestAnalysisRequest().withWholeClasspath(true);
- Class>[] expectedImportResult = new Class[]{getClass()};
+ Class>[] expectedImportResult = new Class[]{this.getClass()};
doReturn(new ClassFileImporter().importClasses(expectedImportResult))
.when(cacheClassFileImporter).importClasses(anySet(), anyCollection());
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"]
----