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..4dacf65f5f 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,11 @@ */ package com.tngtech.archunit.library; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; import com.tngtech.archunit.core.domain.JavaAccess.Functions.Get; @@ -24,6 +29,7 @@ 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.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.DescribedPredicate.not; @@ -37,6 +43,7 @@ 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.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,6 +51,7 @@ 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; @@ -407,4 +415,54 @@ 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 implementation class. + */ + @PublicAPI(usage = ACCESS) + public static ArchRule testClassesShouldResideInTheSamePackageAsImplementation(String testClassPattern) { + return classes().should(new ArchCondition("have the same package as test classes") { + Map simpleTestNameToPackage = new HashMap<>(); + + @Override + public void init(Collection allClasses) { + simpleTestNameToPackage = allClasses.stream() + .filter(clazz -> clazz.getName().endsWith(testClassPattern)) + .collect(Collectors.toMap(JavaClass::getSimpleName, JavaClass::getPackageName)); + } + + @Override + public void check(JavaClass implementationClass, ConditionEvents events) { + final String implementationClassName = implementationClass.getSimpleName(); + final String implementationClassPackageName = implementationClass.getPackageName(); + final String possibleTestClassName = implementationClassName + testClassPattern; + final String possibleTestClassPackageName = simpleTestNameToPackage.get(possibleTestClassName); + + final boolean isTestClassInWrongPackage = possibleTestClassPackageName != null + && !possibleTestClassPackageName.equals(implementationClassPackageName); + + if (isTestClassInWrongPackage) { + events.add( + violated( + implementationClass, + String.format( + "Test class %s.%s is in the wrong package. Should be inside %s package", + possibleTestClassPackageName, possibleTestClassName, implementationClassPackageName + ) + ) + ); + } + } + }); + } + + /** + * A rule that checks that every test class has the same package as implementation class. + */ + @PublicAPI(usage = ACCESS) + public static ArchRule testClassesShouldResideInTheSamePackageAsImplementation() { + final String defaultTestClassSuffix = "Test"; + return testClassesShouldResideInTheSamePackageAsImplementation(defaultTestClassSuffix); + } + } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/TestClassesPackagesAlignedWithImplementationTest.java b/archunit/src/test/java/com/tngtech/archunit/library/TestClassesPackagesAlignedWithImplementationTest.java new file mode 100644 index 0000000000..70764d30c1 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/TestClassesPackagesAlignedWithImplementationTest.java @@ -0,0 +1,60 @@ +package com.tngtech.archunit.library; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.library.testclasses.packages.correct.SecondClass; +import com.tngtech.archunit.library.testclasses.packages.incorrect.FirstClass; +import org.junit.Test; + +import static com.tngtech.archunit.library.GeneralCodingRules.testClassesShouldResideInTheSamePackageAsImplementation; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestClassesPackagesAlignedWithImplementationTest { + + private final JavaClasses correctClasses = new ClassFileImporter().importPackagesOf(SecondClass.class); + private final JavaClasses incorrectClasses = new ClassFileImporter().importPackagesOf(FirstClass.class); + + @Test + public void should_fail_when_test_class_reside_in_different_package_as_implementation() { + // given + final String correctPackage = "com.tngtech.archunit.library.testclasses.packages.incorrect"; + final String incorrectTestClass = "com.tngtech.archunit.library.testclasses.packages.incorrect.dir.FirstClassTest"; + final String expectedErrorMessage = "Test class " + incorrectTestClass + " is in the wrong package. Should be inside " + correctPackage + " package"; + + // when & then + assertThatThrownBy(() -> testClassesShouldResideInTheSamePackageAsImplementation().check(incorrectClasses)) + .isInstanceOf(AssertionError.class) + .hasMessageContaining(expectedErrorMessage); + } + + @Test + public void should_fail_when_test_class_reside_in_different_package_as_implementation_when_have_custom_suffix() { + // given + final String correctPackage = "com.tngtech.archunit.library.testclasses.packages.incorrect"; + final String incorrectTestClass = "com.tngtech.archunit.library.testclasses.packages.incorrect.dir.FirstClassTestingScenario"; + final String expectedErrorMessage = "Test class " + incorrectTestClass + " is in the wrong package. Should be inside " + correctPackage + " package"; + final String testClassesSuffix = "TestingScenario"; + + // when & then + assertThatThrownBy(() -> testClassesShouldResideInTheSamePackageAsImplementation(testClassesSuffix).check(incorrectClasses)) + .isInstanceOf(AssertionError.class) + .hasMessageContaining(expectedErrorMessage); + } + + @Test + public void should_not_fail_when_test_class_and_implementation_class_reside_in_the_same_package() { + // when & then + assertThatNoException().isThrownBy(() -> testClassesShouldResideInTheSamePackageAsImplementation().check(correctClasses)); + } + + @Test + public void should_not_fail_when_test_class_and_implementation_class_reside_in_the_same_package_with_custom_suffix() { + // given + final String testClassesSuffix = "TestingScenario"; + + // when & then + assertThatNoException().isThrownBy(() -> testClassesShouldResideInTheSamePackageAsImplementation(testClassesSuffix).check(correctClasses)); + } + +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClass.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClass.java new file mode 100644 index 0000000000..53e817cdf0 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClass.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.correct; + +public class SecondClass { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClassTest.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClassTest.java new file mode 100644 index 0000000000..0ad322641c --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClassTest.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.correct; + +class SecondClassTest { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClassTestingScenario.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClassTestingScenario.java new file mode 100644 index 0000000000..cbec46cbe0 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/correct/SecondClassTestingScenario.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.correct; + +class SecondClassTestingScenario { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/FirstClass.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/FirstClass.java new file mode 100644 index 0000000000..4c16ba82c0 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/FirstClass.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.incorrect; + +public class FirstClass { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/dir/FirstClassTest.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/dir/FirstClassTest.java new file mode 100644 index 0000000000..f90bd32920 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/dir/FirstClassTest.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.incorrect.dir; + +class FirstClassTest { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/dir/FirstClassTestingScenario.java b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/dir/FirstClassTestingScenario.java new file mode 100644 index 0000000000..d4a0293d43 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/testclasses/packages/incorrect/dir/FirstClassTestingScenario.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.testclasses.packages.incorrect.dir; + +class FirstClassTestingScenario { +}