diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 3495f94..582e97e 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -150,15 +150,16 @@ The default mode is `WITHOUT_TESTS`, which checks only test classes. The default mode is `ONLY_TESTS`, which checks only test classes. -| Category | Method Name | Rule Description | -|---------------|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| -| JUnit 5 | `classesShouldBePackagePrivate` | Ensure that classes whose names match a specific naming pattern are declared as package-private. | -| JUnit 5 | `classesShouldNotBeAnnotatedWithDisabled` | Ensure classes are not annotated with `@Disabled`. | -| JUnit 5 | `methodsShouldBePackagePrivate` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are package-private. | -| JUnit 5 | `methodsShouldNotBeAnnotatedWithDisabled` | Ensure methods are not annotated with `@Disabled`. | -| JUnit 5 | `methodsShouldBeAnnotatedWithDisplayName` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are annotated with `@DisplayName`. | -| JUnit 5 | `methodsShouldMatch` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` have names matching a specific regex pattern. | -| JUnit 5 | `methodsShouldNotDeclareExceptions` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` do not declare any thrown exceptions. | +| Category | Method Name | Rule Description | +|----------|--------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| JUnit 5 | `classesShouldBePackagePrivate` | Ensure that classes whose names match a specific naming pattern are declared as package-private. | +| JUnit 5 | `classesShouldNotBeAnnotatedWithDisabled` | Ensure classes are not annotated with `@Disabled`. | +| JUnit 5 | `methodsShouldBePackagePrivate` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are package-private. | +| JUnit 5 | `methodsShouldNotBeAnnotatedWithDisabled` | Ensure methods are not annotated with `@Disabled`. | +| JUnit 5 | `methodsShouldBeAnnotatedWithDisplayName` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` are annotated with `@DisplayName`. | +| JUnit 5 | `methodsShouldMatch` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` have names matching a specific regex pattern. | +| JUnit 5 | `methodsShouldNotDeclareExceptions` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` do not declare any thrown exceptions. | +| JUnit 5 | `methodsShouldContainAssertionsOrVerifications` | Ensure that test methods annotated with `@Test` or `@ParameterizedTest` contain at least one assertion or verification. | ### Spring Rules @@ -534,6 +535,28 @@ Taikai.builder() .check(); ``` +- **Ensure Test Methods Contain Assertions or Verifications**: : Ensure that test methods annotated with `@Test` or `@ParameterizedTest` contain at least one assertion or verification. + + - **JUnit 5**: Ensure the use of assertions from `org.junit.jupiter.api.Assertions`. + - **Mockito**: Ensure the use of verification methods from `org.mockito.Mockito` like `verify`, `inOrder`, or `capture`. + - **Hamcrest**: Ensure the use of assertions from `org.hamcrest.MatcherAssert`. + - **AssertJ**: Ensure the use of assertions from `org.assertj.core.api.Assertions`. + - **Truth**: Ensure the use of assertions from `com.google.common.truth.Truth`. + - **Cucumber**: Ensure the use of assertions from `io.cucumber.java.en.Then` or `io.cucumber.java.en.Given`. + - **Spring MockMvc**: Ensure the use of assertions from `org.springframework.test.web.servlet.MockMvc` like `andExpect` or `andDo`. + - **ArchUnit**: Ensure the use of the `check` method from `com.tngtech.archunit.lang.ArchRule`. + - **Taikai**: Ensure the use of the `check` method from `com.enofex.taikai.Taikai`. + +```java +Taikai.builder() + .namespace("com.company.project") + .test(test -> test + .junit5(junit5 -> junit5 + .methodsShouldContainAssertionsOrVerifications())) + .build() + .check(); +``` + ## 8. Spring Rules Spring configuration involves defining constraints specific to Spring Framework usage. diff --git a/src/main/java/com/enofex/taikai/test/ContainAssertionsOrVerifications.java b/src/main/java/com/enofex/taikai/test/ContainAssertionsOrVerifications.java new file mode 100644 index 0000000..ceb7556 --- /dev/null +++ b/src/main/java/com/enofex/taikai/test/ContainAssertionsOrVerifications.java @@ -0,0 +1,83 @@ +package com.enofex.taikai.test; + +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.domain.JavaMethodCall; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + +final class ContainAssertionsOrVerifications { + + private ContainAssertionsOrVerifications() { + } + + static ArchCondition containAssertionsOrVerifications() { + return new ArchCondition<>("a unit test should assert or verify something") { + @Override + public void check(JavaMethod item, ConditionEvents events) { + for (JavaMethodCall call : item.getMethodCallsFromSelf()) { + if (jUnit5(call) || + mockito(call) || + hamcrest(call) || + assertJ(call) || + truth(call) || + cucumber(call) || + springMockMvc(call) || + archRule(call) || + taikai(call) + ) { + return; + } + } + events.add(SimpleConditionEvent.violated( + item, + "%s does not assert or verify anything".formatted(item.getDescription())) + ); + } + + private boolean jUnit5(JavaMethodCall call) { + return "org.junit.jupiter.api.Assertions".equals(call.getTargetOwner().getName()); + } + + private boolean mockito(JavaMethodCall call) { + return "org.mockito.Mockito".equals(call.getTargetOwner().getName()) + && (call.getName().startsWith("verify") + || "inOrder".equals(call.getName()) + || "capture".equals(call.getName())); + } + + private boolean hamcrest(JavaMethodCall call) { + return "org.hamcrest.MatcherAssert".equals(call.getTargetOwner().getName()); + } + + private boolean assertJ(JavaMethodCall call) { + return "org.assertj.core.api.Assertions".equals(call.getTargetOwner().getName()); + } + + private boolean truth(JavaMethodCall call) { + return "com.google.common.truth.Truth".equals(call.getTargetOwner().getName()); + } + + private boolean cucumber(JavaMethodCall call) { + return "io.cucumber.java.en.Then".equals(call.getTargetOwner().getName()) || + "io.cucumber.java.en.Given".equals(call.getTargetOwner().getName()); + } + + private boolean springMockMvc(JavaMethodCall call) { + return + "org.springframework.test.web.servlet.MockMvc".equals(call.getTargetOwner().getName()) + && ("andExpect".equals(call.getName()) || "andDo".equals(call.getName())); + } + + private boolean archRule(JavaMethodCall call) { + return "com.tngtech.archunit.lang.ArchRule".equals(call.getTargetOwner().getName()) + && "check".equals(call.getName()); + } + + private boolean taikai(JavaMethodCall call) { + return "com.enofex.taikai.Taikai".equals(call.getTargetOwner().getName()) + && "check".equals(call.getName()); + } + }; + } +} diff --git a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java index bf25e49..8c6626d 100644 --- a/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java +++ b/src/main/java/com/enofex/taikai/test/JUnit5Configurer.java @@ -1,12 +1,12 @@ package com.enofex.taikai.test; import static com.enofex.taikai.internal.ArchConditions.notDeclareThrownExceptions; +import static com.enofex.taikai.test.ContainAssertionsOrVerifications.containAssertionsOrVerifications; import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_DISABLED; import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_DISPLAY_NAME; import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_PARAMETRIZED_TEST; import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_TEST; import static com.enofex.taikai.test.JUnit5DescribedPredicates.annotatedWithTestOrParameterizedTest; - import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; @@ -40,18 +40,6 @@ public JUnit5Configurer methodsShouldMatch(String regex, Configuration configura configuration)); } - public JUnit5Configurer classesShouldBePackagePrivate(String regex) { - return classesShouldBePackagePrivate(regex, CONFIGURATION); - } - - public JUnit5Configurer classesShouldBePackagePrivate(String regex, Configuration configuration) { - return addRule(TaikaiRule.of(classes() - .that().areNotInterfaces().and().haveNameMatching(regex) - .should().bePackagePrivate() - .as("Classes with names matching %s should be package-private".formatted(regex)), - configuration)); - } - public JUnit5Configurer methodsShouldNotDeclareExceptions() { return methodsShouldNotDeclareExceptions(CONFIGURATION); } @@ -102,10 +90,33 @@ public JUnit5Configurer methodsShouldNotBeAnnotatedWithDisabled(Configuration co configuration)); } + public JUnit5Configurer methodsShouldContainAssertionsOrVerifications() { + return methodsShouldContainAssertionsOrVerifications(CONFIGURATION); + } + + public JUnit5Configurer methodsShouldContainAssertionsOrVerifications(Configuration configuration) { + return addRule(TaikaiRule.of(methods() + .that(are(annotatedWithTestOrParameterizedTest(true))) + .should(containAssertionsOrVerifications()), configuration)); + } + public JUnit5Configurer classesShouldNotBeAnnotatedWithDisabled() { return classesShouldNotBeAnnotatedWithDisabled(CONFIGURATION); } + public JUnit5Configurer classesShouldBePackagePrivate(String regex) { + return classesShouldBePackagePrivate(regex, CONFIGURATION); + } + + public JUnit5Configurer classesShouldBePackagePrivate(String regex, Configuration configuration) { + return addRule(TaikaiRule.of(classes() + .that().areNotInterfaces().and().haveNameMatching(regex) + .should().bePackagePrivate() + .as("Classes with names matching %s should be package-private".formatted(regex)), + configuration)); + } + + public JUnit5Configurer classesShouldNotBeAnnotatedWithDisabled(Configuration configuration) { return addRule(TaikaiRule.of(noClasses() .should().beMetaAnnotatedWith(ANNOTATION_DISABLED) diff --git a/src/test/java/com/enofex/taikai/ArchitectureTest.java b/src/test/java/com/enofex/taikai/ArchitectureTest.java index 471f231..196fe7b 100644 --- a/src/test/java/com/enofex/taikai/ArchitectureTest.java +++ b/src/test/java/com/enofex/taikai/ArchitectureTest.java @@ -40,7 +40,8 @@ void shouldFulfilConstrains() { .methodsShouldNotBeAnnotatedWithDisabled() .methodsShouldMatch("should.*") .methodsShouldBePackagePrivate() - .methodsShouldNotDeclareExceptions())) + .methodsShouldNotDeclareExceptions() + .methodsShouldContainAssertionsOrVerifications())) .build() .check(); } diff --git a/src/test/java/com/enofex/taikai/Usage.java b/src/test/java/com/enofex/taikai/Usage.java index 604d7ee..f8779e3 100644 --- a/src/test/java/com/enofex/taikai/Usage.java +++ b/src/test/java/com/enofex/taikai/Usage.java @@ -68,6 +68,7 @@ public static void main(String[] args) { .methodsShouldBePackagePrivate() .methodsShouldBeAnnotatedWithDisplayName() .methodsShouldNotBeAnnotatedWithDisabled() + .methodsShouldContainAssertionsOrVerifications() .classesShouldBePackagePrivate(".*Test") .classesShouldNotBeAnnotatedWithDisabled())) .spring(spring -> spring diff --git a/src/test/java/com/enofex/taikai/test/JUnit5DescribedPredicatesTest.java b/src/test/java/com/enofex/taikai/test/JUnit5DescribedPredicatesTest.java index c814b56..e13661a 100644 --- a/src/test/java/com/enofex/taikai/test/JUnit5DescribedPredicatesTest.java +++ b/src/test/java/com/enofex/taikai/test/JUnit5DescribedPredicatesTest.java @@ -6,6 +6,7 @@ import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; @@ -45,6 +46,7 @@ private static final class TestExample { @Test void should() { + assertTrue(true); } } @@ -53,6 +55,7 @@ private static final class ParameterizedTestExample { @ParameterizedTest @EmptySource void should(String empty) { + assertTrue(true); } } @@ -60,6 +63,7 @@ private static class MetaTestExample { @TestAnnotation void should() { + assertTrue(true); } } @@ -68,14 +72,17 @@ private static final class MetaParameterizedTestExample { @ParameterizedTestAnnotation @EmptySource void should(String empty) { + assertTrue(true); } } @Test private @interface TestAnnotation { + } @ParameterizedTest private @interface ParameterizedTestAnnotation { + } }