diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 5d5a52f..b6fefa7 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -19,43 +19,44 @@ Architecture rules are defined using Taikai's fluent API, allowing developers to ## 3. Usage -| Category | Subcategory | Method Name | Rule Description | Import Options | -|------------|----------------|--------------------------------------------------------|---------------------------------------------------------------------------------|-------------------------| -| **Java** | General | `classesShouldImplementHashCodeAndEquals` | Classes should implement `hashCode` and `equals` | Default (WITHOUT_TESTS) | -| | General | `fieldsShouldNotBePublic` | Fields should not be `public` (except constants) | Default (WITHOUT_TESTS) | -| | General | `methodsShouldNotThrowGenericException` | Methods should not throw generic exceptions (`Exception`, `RuntimeException`) | Default (WITHOUT_TESTS) | -| | General | `noUsageOf` | Disallow usage of specific classes | Default (WITHOUT_TESTS) | -| | General | `noUsageOf` | Disallow usage of specific classes by class reference | Default (WITHOUT_TESTS) | -| | General | `noUsageOfDeprecatedAPIs` | No usage of deprecated APIs annotated with `Deprecated` | Default (WITHOUT_TESTS) | -| | General | `noUsageOfSystemOutOrErr` | Disallow usage of `System.out` or `System.err` | Default (WITHOUT_TESTS) | -| | General | `utilityClassesShouldBeFinalAndHavePrivateConstructor` | Utility classes should be `final` and have a private constructor | Default (WITHOUT_TESTS) | -| | Imports | `shouldHaveNoCycles` | No cyclic dependencies in imports | Default (WITHOUT_TESTS) | -| | Imports | `shouldNotImport` | Disallow specific imports (e.g., `..shaded..`) | Default (WITHOUT_TESTS) | -| | Naming | `classesShouldNotMatch` | Classes should not match specific naming patterns (e.g., `.*Impl`) | Default (WITHOUT_TESTS) | -| | Naming | `methodsShouldNotMatch` | Methods should not match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `fieldsShouldNotMatch` | Fields should not match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `classesAnnotatedWithShouldMatch` | Classes annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `methodsAnnotatedWithShouldMatch` | Methods annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `fieldsAnnotatedWithShouldMatch` | Fields annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | -| | Naming | `constantsShouldFollowConvention` | Constants should follow naming conventions | Default (WITHOUT_TESTS) | -| | Naming | `interfacesShouldNotHavePrefixI` | Interfaces should not have the prefix `I` | Default (WITHOUT_TESTS) | -| **Test** | JUnit 5 | `jclassesShouldNotBeAnnotatedWithDisabled` | Ensure JUnit 5 classes are not annotated with `@Disabled` | Default (WITH_TESTS) | -| | JUnit 5 | `jmethodsShouldNotBeAnnotatedWithDisabled` | Ensure JUnit 5 methods are not annotated with `@Disabled` | Default (WITH_TESTS) | -| **Spring** | General | `noAutowiredFields` | Fields should not be annotated with `@Autowired` (prefer constructor injection) | Default (WITH_TESTS) | -| | Boot | `springBootApplicationShouldBeIn` | Ensure `@SpringBootApplication` is in the default package | Default (WITH_TESTS) | -| | Configurations | `namesShouldEndWithConfiguration` | Configuration classes should end with "Configuration" | Default (WITH_TESTS) | -| | Configurations | `namesShouldMatch` | Configuration classes should match a regex pattern | Default (WITH_TESTS) | -| | Controllers | `namesShouldEndWithController` | Controllers should end with "Controller" | Default (WITH_TESTS) | -| | Controllers | `namesShouldMatch` | Controllers should match a regex pattern | Default (WITH_TESTS) | -| | Controllers | `shouldBeAnnotatedWithRestController` | Controllers should be annotated with `@RestController` | Default (WITH_TESTS) | -| | Controllers | `shouldBePackagePrivate` | Controllers should be package-private | Default (WITH_TESTS) | -| | Controllers | `shouldNotDependOnOtherControllers` | Controllers should not depend on other controllers | Default (WITH_TESTS) | -| | Repositories | `namesShouldEndWithRepository` | Repositories should end with "Repository" | Default (WITH_TESTS) | -| | Repositories | `namesShouldMatch` | Repositories should match a regex pattern | Default (WITH_TESTS) | -| | Repositories | `shouldBeAnnotatedWithRepository` | Repositories should be annotated with `@Repository` | Default (WITH_TESTS) | -| | Services | `namesShouldEndWithService` | Services should end with "Service" | Default (WITH_TESTS) | -| | Services | `namesShouldMatch` | Services should match a regex pattern | Default (WITH_TESTS) | -| | Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | Default (WITH_TESTS) | +| Category | Subcategory | Method Name | Rule Description | Import Options | +|------------|----------------|--------------------------------------------------------|----------------------------------------------------------------------------------------------|-------------------------| +| **Java** | General | `classesShouldImplementHashCodeAndEquals` | Classes should implement `hashCode` and `equals` | Default (WITHOUT_TESTS) | +| | General | `fieldsShouldNotBePublic` | Fields should not be `public` (except constants) | Default (WITHOUT_TESTS) | +| | General | `methodsShouldNotThrowGenericException` | Methods should not throw generic exceptions (`Exception`, `RuntimeException`) | Default (WITHOUT_TESTS) | +| | General | `noUsageOf` | Disallow usage of specific classes | Default (WITHOUT_TESTS) | +| | General | `noUsageOf` | Disallow usage of specific classes by class reference | Default (WITHOUT_TESTS) | +| | General | `noUsageOfDeprecatedAPIs` | No usage of deprecated APIs annotated with `Deprecated` | Default (WITHOUT_TESTS) | +| | General | `noUsageOfSystemOutOrErr` | Disallow usage of `System.out` or `System.err` | Default (WITHOUT_TESTS) | +| | General | `utilityClassesShouldBeFinalAndHavePrivateConstructor` | Utility classes should be `final` and have a private constructor | Default (WITHOUT_TESTS) | +| | Imports | `shouldHaveNoCycles` | No cyclic dependencies in imports | Default (WITHOUT_TESTS) | +| | Imports | `shouldNotImport` | Disallow specific imports (e.g., `..shaded..`) | Default (WITHOUT_TESTS) | +| | Naming | `classesShouldNotMatch` | Classes should not match specific naming patterns (e.g., `.*Impl`) | Default (WITHOUT_TESTS) | +| | Naming | `methodsShouldNotMatch` | Methods should not match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `fieldsShouldNotMatch` | Fields should not match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `classesAnnotatedWithShouldMatch` | Classes annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `methodsAnnotatedWithShouldMatch` | Methods annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `fieldsAnnotatedWithShouldMatch` | Fields annotated with should match specific naming patterns | Default (WITHOUT_TESTS) | +| | Naming | `constantsShouldFollowConvention` | Constants should follow naming conventions | Default (WITHOUT_TESTS) | +| | Naming | `interfacesShouldNotHavePrefixI` | Interfaces should not have the prefix `I` | Default (WITHOUT_TESTS) | +| **Test** | JUnit 5 | `classesShouldNotBeAnnotatedWithDisabled` | Ensure classes are not annotated with `@Disabled` | Default (WITH_TESTS) | +| | JUnit 5 | `methodsShouldNotBeAnnotatedWithDisabled` | Ensure methods are not annotated with `@Disabled` | Default (WITH_TESTS) | +| | JUnit 5 | `methodsShouldBePackagePrivate` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are package-private. | Default (WITH_TESTS) | +| **Spring** | General | `noAutowiredFields` | Fields should not be annotated with `@Autowired` (prefer constructor injection) | Default (WITH_TESTS) | +| | Boot | `springBootApplicationShouldBeIn` | Ensure `@SpringBootApplication` is in the default package | Default (WITH_TESTS) | +| | Configurations | `namesShouldEndWithConfiguration` | Configuration classes should end with "Configuration" | Default (WITH_TESTS) | +| | Configurations | `namesShouldMatch` | Configuration classes should match a regex pattern | Default (WITH_TESTS) | +| | Controllers | `namesShouldEndWithController` | Controllers should end with "Controller" | Default (WITH_TESTS) | +| | Controllers | `namesShouldMatch` | Controllers should match a regex pattern | Default (WITH_TESTS) | +| | Controllers | `shouldBeAnnotatedWithRestController` | Controllers should be annotated with `@RestController` | Default (WITH_TESTS) | +| | Controllers | `shouldBePackagePrivate` | Controllers should be package-private | Default (WITH_TESTS) | +| | Controllers | `shouldNotDependOnOtherControllers` | Controllers should not depend on other controllers | Default (WITH_TESTS) | +| | Repositories | `namesShouldEndWithRepository` | Repositories should end with "Repository" | Default (WITH_TESTS) | +| | Repositories | `namesShouldMatch` | Repositories should match a regex pattern | Default (WITH_TESTS) | +| | Repositories | `shouldBeAnnotatedWithRepository` | Repositories should be annotated with `@Repository` | Default (WITH_TESTS) | +| | Services | `namesShouldEndWithService` | Services should end with "Service" | Default (WITH_TESTS) | +| | Services | `namesShouldMatch` | Services should match a regex pattern | Default (WITH_TESTS) | +| | Services | `shouldBeAnnotatedWithService` | Services should be annotated with `@Service` | Default (WITH_TESTS) | ### Java Configuration @@ -199,6 +200,18 @@ Taikai.builder() .check(); ``` +- **Ensure Test Methods are Package-Private**: Ensure that JUnit 5 test methods annotated with `@Test` or `@ParameterizedTest` are package-private. + +```java +Taikai.builder() + .namespace("com.company.yourproject") + .test(test -> test + .junit5(junit5 -> junit5 + .methodsShouldBePackagePrivate())) + .build() + .check(); +``` + ### Spring Configuration Spring configuration involves defining constraints specific to Spring Framework usage. diff --git a/src/main/java/com/enofex/taikai/AnnotationPredicates.java b/src/main/java/com/enofex/taikai/AnnotationPredicates.java new file mode 100644 index 0000000..ad1d83c --- /dev/null +++ b/src/main/java/com/enofex/taikai/AnnotationPredicates.java @@ -0,0 +1,21 @@ +package com.enofex.taikai; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; + +public final class AnnotationPredicates { + + private AnnotationPredicates() { + } + + public static DescribedPredicate annotatedWith(String annotation, + boolean isMetaAnnotated) { + return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { + @Override + public boolean test(CanBeAnnotated canBeAnnotated) { + return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) : + canBeAnnotated.isAnnotatedWith(annotation); + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/spring/SpringPredicates.java b/src/main/java/com/enofex/taikai/spring/SpringPredicates.java index 6fca310..6382f11 100644 --- a/src/main/java/com/enofex/taikai/spring/SpringPredicates.java +++ b/src/main/java/com/enofex/taikai/spring/SpringPredicates.java @@ -1,5 +1,7 @@ package com.enofex.taikai.spring; +import static com.enofex.taikai.AnnotationPredicates.annotatedWith; + import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; @@ -52,15 +54,4 @@ static DescribedPredicate annotatedWithSpringBootApplication( static DescribedPredicate annotatedAutowired(boolean isMetaAnnotated) { return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated); } - - private static DescribedPredicate annotatedWith(String annotation, - boolean isMetaAnnotated) { - return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { - @Override - public boolean test(CanBeAnnotated canBeAnnotated) { - return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) : - canBeAnnotated.isAnnotatedWith(annotation); - } - }; - } } diff --git a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java index dccdaa9..027dddc 100644 --- a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java +++ b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java @@ -1,5 +1,10 @@ package com.enofex.taikai.test; +import static com.enofex.taikai.test.JUnit5Predicates.ANNOTATION_PARAMETRIZED_TEST; +import static com.enofex.taikai.test.JUnit5Predicates.ANNOTATION_TEST; +import static com.enofex.taikai.test.JUnit5Predicates.annotatedWithTestOrParameterizedTest; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noMethods; @@ -17,6 +22,19 @@ public final class JUnit5Configurer extends AbstractConfigurer { super(configurerContext); } + public JUnit5Configurer methodsShouldBePackagePrivate() { + return methodsShouldBePackagePrivate(Configuration.of(Namespace.IMPORT.WITH_TESTS)); + } + + public JUnit5Configurer methodsShouldBePackagePrivate(Configuration configuration) { + return addRule(TaikaiRule.of(methods() + .that(are(annotatedWithTestOrParameterizedTest(true))) + .should().bePackagePrivate() + .as("Methods annotated with %s or %s should be package-private".formatted(ANNOTATION_TEST, + ANNOTATION_PARAMETRIZED_TEST)), + configuration)); + } + public JUnit5Configurer methodsShouldNotBeAnnotatedWithDisabled() { return methodsShouldNotBeAnnotatedWithDisabled(Configuration.of(Namespace.IMPORT.WITH_TESTS)); } diff --git a/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java b/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java new file mode 100644 index 0000000..c8f1fab --- /dev/null +++ b/src/main/java/com/enofex/taikai/test/JUnit5Predicates.java @@ -0,0 +1,22 @@ +package com.enofex.taikai.test; + +import static com.enofex.taikai.AnnotationPredicates.annotatedWith; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; + +final class JUnit5Predicates { + + static final String ANNOTATION_TEST = "org.junit.jupiter.api.Test"; + static final String ANNOTATION_PARAMETRIZED_TEST = "org.junit.jupiter.params.ParameterizedTest"; + + private JUnit5Predicates() { + } + + static DescribedPredicate annotatedWithTestOrParameterizedTest( + boolean isMetaAnnotated) { + + return annotatedWith(ANNOTATION_TEST, isMetaAnnotated) + .or(annotatedWith(ANNOTATION_PARAMETRIZED_TEST, isMetaAnnotated)); + } +} diff --git a/src/test/java/com/enofex/taikai/ArchitectureTest.java b/src/test/java/com/enofex/taikai/ArchitectureTest.java index 5677d42..0ed4fbe 100644 --- a/src/test/java/com/enofex/taikai/ArchitectureTest.java +++ b/src/test/java/com/enofex/taikai/ArchitectureTest.java @@ -16,6 +16,7 @@ void shouldFulfilConstrains() { .namespace("com.enofex.taikai") .test(test -> test .junit5(junit5 -> junit5 + .methodsShouldBePackagePrivate() .classesShouldNotBeAnnotatedWithDisabled() .methodsShouldNotBeAnnotatedWithDisabled())) .java(java -> java diff --git a/src/test/java/com/enofex/taikai/Usage.java b/src/test/java/com/enofex/taikai/Usage.java index 761c248..529925b 100644 --- a/src/test/java/com/enofex/taikai/Usage.java +++ b/src/test/java/com/enofex/taikai/Usage.java @@ -31,6 +31,7 @@ public static void main(String[] args) { .namesShouldEndWithRepository())) .test(test -> test .junit5(junit5 -> junit5 + .methodsShouldBePackagePrivate() .classesShouldNotBeAnnotatedWithDisabled() .methodsShouldNotBeAnnotatedWithDisabled())) .java(java -> java