Skip to content

Commit

Permalink
Add rule checking test and implementation class are in same package #908
Browse files Browse the repository at this point in the history


This will add the library rule `GeneralCodingRules.testClassesShouldResideInTheSamePackageAsImplementation(..)` which will test that implementation and test class reside in the same package. The rule can e.g. detect mismatches like `com.myapp.correct.SomeClass` and `com.myapp.wrong.SomeClassTest`.

Resolves: #475
  • Loading branch information
codecholeric authored Jul 13, 2022
2 parents b3a47b5 + ee1e420 commit 01f1626
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -37,15 +43,19 @@
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;
import static com.tngtech.archunit.lang.conditions.ArchConditions.callMethodWhere;
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}
Expand Down Expand Up @@ -407,4 +417,63 @@ private static ArchCondition<JavaField> 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.<br>
* 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}).<br>
* 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.<br>
* 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<JavaClass> resideInTheSamePackageAsTheirTestClasses(String testClassSuffix) {
return new ArchCondition<JavaClass>("reside in the same package as their test classes") {
Map<String, JavaClass> testClassesBySimpleClassName = new HashMap<>();

@Override
public void init(Collection<JavaClass> 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));
}
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.library.testclasses.packages.correct;

public class ImplementationClassWithCorrectPackage {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.library.testclasses.packages.correct;

public class ImplementationClassWithCorrectPackageTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.library.testclasses.packages.correct;

public class ImplementationClassWithCorrectPackageTestingScenario {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.library.testclasses.packages.incorrect;

public class ImplementationClassWithWrongTestClassPackage {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.library.testclasses.packages.incorrect.wrongsubdir;

public class ImplementationClassWithWrongTestClassPackageTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.library.testclasses.packages.incorrect.wrongsubdir;

public class ImplementationClassWithWrongTestClassPackageTestingScenario {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 01f1626

Please sign in to comment.