diff --git a/archunit/src/main/java/com/tngtech/archunit/library/GeneralCodingRules.java b/archunit/src/main/java/com/tngtech/archunit/library/GeneralCodingRules.java index 800ad0bab8..62a96b6f9c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/GeneralCodingRules.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/GeneralCodingRules.java @@ -15,6 +15,10 @@ */ package com.tngtech.archunit.library; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; import com.tngtech.archunit.core.domain.JavaAccess.Functions.Get; @@ -24,7 +28,9 @@ import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import static com.google.common.base.Functions.identity; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.AccessTarget.Predicates.constructor; @@ -37,6 +43,8 @@ import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner; import static com.tngtech.archunit.core.domain.properties.HasParameterTypes.Predicates.rawParameterTypes; import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; +import static com.tngtech.archunit.lang.ConditionEvent.createMessage; +import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; import static com.tngtech.archunit.lang.conditions.ArchConditions.accessField; import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith; import static com.tngtech.archunit.lang.conditions.ArchConditions.callCodeUnitWhere; @@ -44,8 +52,10 @@ import static com.tngtech.archunit.lang.conditions.ArchConditions.dependOnClassesThat; import static com.tngtech.archunit.lang.conditions.ArchConditions.setFieldWhere; import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields; +import static java.util.stream.Collectors.toMap; /** * GeneralCodingRules provides a set of very general {@link ArchCondition ArchConditions} @@ -407,4 +417,63 @@ private static ArchCondition beAnnotatedWithAnInjectionAnnotation() { .as("no classes should use field injection") .because("field injection is considered harmful; use constructor injection or setter injection instead; " + "see https://stackoverflow.com/q/39890849 for detailed explanations"); + + /** + * A rule that checks that every test class has the same package as the implementation class.
+ * The rule assumes that tests can be identified by having the same name as the implementation class, + * but suffixed with "Test" (e.g. {@code SomeClass} -> {@code SomeClassTest}).
+ * To customize the name suffix that identifies test classes please refer to + * {@link #testClassesShouldResideInTheSamePackageAsImplementation(String)} + */ + @PublicAPI(usage = ACCESS) + public static ArchRule testClassesShouldResideInTheSamePackageAsImplementation() { + return testClassesShouldResideInTheSamePackageAsImplementation("Test"); + } + + /** + * A rule that checks that every test class resides in the same package as the implementation class.
+ * This rule will identify "test classes" solely by class name convention. I.e. for a given + * class {@code SomeObject} the respective test class will be derived as {@code SomeObject${testClassSuffix}} + * taking into account the supplied {@code testClassSuffix}. If the {@code testClassSuffix} + * would for example be {@code "Tests"}, then {@code SomeObjectTests} would be identified as the associated test class + * of {@code SomeObject}. + * + * @param testClassSuffix The suffix that distinguishes test classes from their respective implementation class under test, e.g. {@code "Test"} + * @see #testClassesShouldResideInTheSamePackageAsImplementation() + */ + @PublicAPI(usage = ACCESS) + public static ArchRule testClassesShouldResideInTheSamePackageAsImplementation(String testClassSuffix) { + return classes().should(resideInTheSamePackageAsTheirTestClasses(testClassSuffix)) + .as("test classes should reside in the same package as their implementation classes"); + } + + private static ArchCondition resideInTheSamePackageAsTheirTestClasses(String testClassSuffix) { + return new ArchCondition("reside in the same package as their test classes") { + Map testClassesBySimpleClassName = new HashMap<>(); + + @Override + public void init(Collection allClasses) { + testClassesBySimpleClassName = allClasses.stream() + .filter(clazz -> clazz.getName().endsWith(testClassSuffix)) + .collect(toMap(JavaClass::getSimpleName, identity())); + } + + @Override + public void check(JavaClass implementationClass, ConditionEvents events) { + String implementationClassName = implementationClass.getSimpleName(); + String implementationClassPackageName = implementationClass.getPackageName(); + String possibleTestClassName = implementationClassName + testClassSuffix; + JavaClass possibleTestClass = testClassesBySimpleClassName.get(possibleTestClassName); + + boolean isTestClassInWrongPackage = possibleTestClass != null + && !possibleTestClass.getPackageName().equals(implementationClassPackageName); + + if (isTestClassInWrongPackage) { + String message = createMessage(possibleTestClass, + String.format("does not reside in same package as implementation class <%s>", implementationClass.getName())); + events.add(violated(possibleTestClass, message)); + } + } + }; + } } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java b/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java new file mode 100644 index 0000000000..3baf5dd3b2 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java @@ -0,0 +1,44 @@ +package com.tngtech.archunit.library; + +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.library.testclasses.packages.correct.ImplementationClassWithCorrectPackage; +import com.tngtech.archunit.library.testclasses.packages.incorrect.ImplementationClassWithWrongTestClassPackage; +import com.tngtech.archunit.library.testclasses.packages.incorrect.wrongsubdir.ImplementationClassWithWrongTestClassPackageTest; +import com.tngtech.archunit.library.testclasses.packages.incorrect.wrongsubdir.ImplementationClassWithWrongTestClassPackageTestingScenario; +import org.junit.Test; + +import static com.tngtech.archunit.library.GeneralCodingRules.testClassesShouldResideInTheSamePackageAsImplementation; +import static com.tngtech.archunit.testutil.Assertions.assertThatRule; + +public class GeneralCodingRulesTest { + + @Test + public void test_class_in_same_package_should_fail_when_test_class_reside_in_different_package_as_implementation() { + assertThatRule(testClassesShouldResideInTheSamePackageAsImplementation()) + .checking(new ClassFileImporter().importPackagesOf(ImplementationClassWithWrongTestClassPackage.class)) + .hasOnlyOneViolationWithStandardPattern(ImplementationClassWithWrongTestClassPackageTest.class, + "does not reside in same package as implementation class <" + ImplementationClassWithWrongTestClassPackage.class.getName() + ">"); + } + + @Test + public void test_class_in_same_package_should_fail_when_test_class_reside_in_different_package_as_implementation_with_custom_suffix() { + assertThatRule(testClassesShouldResideInTheSamePackageAsImplementation("TestingScenario")) + .checking(new ClassFileImporter().importPackagesOf(ImplementationClassWithWrongTestClassPackage.class)) + .hasOnlyOneViolationWithStandardPattern(ImplementationClassWithWrongTestClassPackageTestingScenario.class, + "does not reside in same package as implementation class <" + ImplementationClassWithWrongTestClassPackage.class.getName() + ">"); + } + + @Test + public void test_class_in_same_package_should_pass_when_test_class_and_implementation_class_reside_in_the_same_package() { + assertThatRule(testClassesShouldResideInTheSamePackageAsImplementation()) + .checking(new ClassFileImporter().importPackagesOf(ImplementationClassWithCorrectPackage.class)) + .hasNoViolation(); + } + + @Test + public void test_class_in_same_package_should_pass_when_test_class_and_implementation_class_reside_in_the_same_package_with_custom_suffix() { + assertThatRule(testClassesShouldResideInTheSamePackageAsImplementation("TestingScenario")) + .checking(new ClassFileImporter().importPackagesOf(ImplementationClassWithCorrectPackage.class)) + .hasNoViolation(); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackage.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackage.java new file mode 100644 index 0000000000..6d98573e40 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackage.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.correct; + +public class ImplementationClassWithCorrectPackage { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackageTest.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackageTest.java new file mode 100644 index 0000000000..30db61041d --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackageTest.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.correct; + +public class ImplementationClassWithCorrectPackageTest { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackageTestingScenario.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackageTestingScenario.java new file mode 100644 index 0000000000..54d91cf9bd --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/ImplementationClassWithCorrectPackageTestingScenario.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.correct; + +public class ImplementationClassWithCorrectPackageTestingScenario { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/ImplementationClassWithWrongTestClassPackage.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/ImplementationClassWithWrongTestClassPackage.java new file mode 100644 index 0000000000..7774b20174 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/ImplementationClassWithWrongTestClassPackage.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.incorrect; + +public class ImplementationClassWithWrongTestClassPackage { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/wrongsubdir/ImplementationClassWithWrongTestClassPackageTest.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/wrongsubdir/ImplementationClassWithWrongTestClassPackageTest.java new file mode 100644 index 0000000000..d139f6f695 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/wrongsubdir/ImplementationClassWithWrongTestClassPackageTest.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.incorrect.wrongsubdir; + +public class ImplementationClassWithWrongTestClassPackageTest { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/wrongsubdir/ImplementationClassWithWrongTestClassPackageTestingScenario.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/wrongsubdir/ImplementationClassWithWrongTestClassPackageTestingScenario.java new file mode 100644 index 0000000000..663aedfa46 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/wrongsubdir/ImplementationClassWithWrongTestClassPackageTestingScenario.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.incorrect.wrongsubdir; + +public class ImplementationClassWithWrongTestClassPackageTestingScenario { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java index ca7d3ee7fa..3c5763deb1 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java @@ -55,6 +55,11 @@ public ArchRuleCheckAssertion hasOnlyOneViolation(String violationMessage) { return this; } + public ArchRuleCheckAssertion hasOnlyOneViolationWithStandardPattern(Class violatingClass, String violationDescription) { + String violationMessage = "Class <" + violatingClass.getName() + "> " + violationDescription + " in (" + violatingClass.getSimpleName() + ".java:0)"; + return hasOnlyOneViolation(violationMessage); + } + @SuppressWarnings("OptionalGetWithoutIsPresent") public ArchRuleCheckAssertion hasOnlyOneViolationMatching(String regex) { assertThat(getOnlyElement(evaluationResult.getFailureReport().getDetails())).matches(regex);