diff --git a/src/main/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsed.java b/src/main/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsed.java index fd0f5ede3..0cc69d9c9 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsed.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsed.java @@ -41,12 +41,14 @@ public class AddMockitoExtensionIfAnnotationsUsed extends Recipe { final String displayName = "Adds Mockito extensions to Mockito tests"; @Getter - final String description = "Adds `@ExtendWith(MockitoExtension.class)` to tests using `@Mock` or `@Captor`."; + final String description = "Adds `@ExtendWith(MockitoExtension.class)` to JUnit 5 tests or " + + "`@RunWith(MockitoJUnitRunner.class)` to JUnit 4 tests using Mockito annotations like `@Mock` or `@Captor`."; @Override public TreeVisitor getVisitor() { TreeVisitor hasExtendedWithAnnotation = new FindAnnotations("org.junit.jupiter.api.extension.ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)", false).getVisitor(); + TreeVisitor hasRunWithAnnotation = new FindAnnotations("org.junit.runner.RunWith", false).getVisitor(); @SuppressWarnings("unchecked") TreeVisitor[] hasAnyMockitoAnnotation = new TreeVisitor[]{ // see https://www.baeldung.com/mockito-annotations for examples @@ -57,16 +59,23 @@ public TreeVisitor getVisitor() { }; return check(and(new IsLikelyTest().getVisitor(), - // check to only migrate JUnit 5 tests - new FindTypes("org.junit.jupiter..*", false).getVisitor(), - // prevent addition if present - not(hasExtendedWithAnnotation), - or(hasAnyMockitoAnnotation)), + or(hasAnyMockitoAnnotation), + or( + // JUnit 5: has JUnit 5 types and no @ExtendWith(MockitoExtension.class) + and(new FindTypes("org.junit.jupiter..*", false).getVisitor(), + not(hasExtendedWithAnnotation)), + // JUnit 4: has @org.junit.Test, no @RunWith + and(new FindAnnotations("org.junit.Test", false).getVisitor(), + not(hasRunWithAnnotation)) + )), new TreeVisitor() { @Override public @Nullable Tree preVisit(Tree tree, ExecutionContext ctx) { stopAfterPreVisit(); if (tree instanceof J.CompilationUnit) { + if (!FindAnnotations.find((J) tree, "@org.junit.Test").isEmpty()) { + return getJunit4JavaVisitor().visit(tree, ctx); + } return getJavaVisitor().visit(tree, ctx); } if (tree instanceof K.CompilationUnit) { @@ -94,6 +103,23 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex }; } + private JavaIsoVisitor getJunit4JavaVisitor() { + return new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + maybeAddImport("org.mockito.junit.MockitoJUnitRunner"); + maybeAddImport("org.junit.runner.RunWith"); + + return JavaTemplate.builder("@RunWith(MockitoJUnitRunner.class)") + .imports("org.mockito.junit.MockitoJUnitRunner") + .imports("org.junit.runner.RunWith") + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-4", "mockito-core")) + .build() + .apply(getCursor(), classDecl.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); + } + }; + } + private KotlinIsoVisitor getKotlinVisitor() { return new KotlinIsoVisitor() { @Override diff --git a/src/test/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsedTest.java b/src/test/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsedTest.java index f9af500aa..877d203d6 100644 --- a/src/test/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsedTest.java +++ b/src/test/java/org/openrewrite/java/testing/mockito/AddMockitoExtensionIfAnnotationsUsedTest.java @@ -33,7 +33,7 @@ class AddMockitoExtensionIfAnnotationsUsedTest implements RewriteTest { public void defaults(RecipeSpec spec) { spec.recipe(new AddMockitoExtensionIfAnnotationsUsed()) .parser(JavaParser.fromJavaVersion() - .classpathFromResources(new InMemoryExecutionContext(), "junit-jupiter-api", "mockito-junit-jupiter", "mockito-core") + .classpathFromResources(new InMemoryExecutionContext(), "junit-4", "junit-jupiter-api", "mockito-junit-jupiter", "mockito-core") .dependsOn("public class Service {}")) .parser(KotlinParser.builder() .classpathFromResources(new InMemoryExecutionContext(), "junit-jupiter-api", "mockito-junit-jupiter", "mockito-core") @@ -42,7 +42,7 @@ public void defaults(RecipeSpec spec) { @DocumentExample @Test - void addForMock() { + void addForMockWithJUnit5() { rewriteRun( //language=java java( @@ -76,7 +76,7 @@ void test() {} } @Test - void addForCaptor() { + void addForCaptorWithJUnit5() { rewriteRun( //language=java java( @@ -110,7 +110,7 @@ void test() {} } @Test - void dontAddIfPresent() { + void doNotAddIfPresentWithJUnit5() { rewriteRun( //language=java java( @@ -133,21 +133,56 @@ class Test { } @Test - void dontAddIfJunit4() { + void addForMockWithJUnit4() { rewriteRun( //language=java java( """ import org.junit.Test; - import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; - import org.mockito.junit.jupiter.MockitoExtension; - class Test { + public class MyTest { @Mock Service service; @Test - void test() {} + public void test() {} + } + """, + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.mockito.Mock; + import org.mockito.junit.MockitoJUnitRunner; + + @RunWith(MockitoJUnitRunner.class) + public class MyTest { + @Mock + Service service; + @Test + public void test() {} + } + """ + ) + ); + } + + @Test + void doNotAddIfPresentWithJUnit4() { + rewriteRun( + //language=java + java( + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.mockito.Mock; + import org.mockito.junit.MockitoJUnitRunner; + + @RunWith(MockitoJUnitRunner.class) + public class MyTest { + @Mock + Service service; + @Test + public void test() {} } """ ) @@ -155,7 +190,30 @@ void test() {} } @Test - void notInferWithExistingAnnotations() { + void doNotAddIfOtherRunnerPresentWithJUnit4() { + rewriteRun( + //language=java + java( + """ + import org.junit.Test; + import org.junit.runner.RunWith; + import org.junit.runners.JUnit4; + import org.mockito.Mock; + + @RunWith(JUnit4.class) + public class MyTest { + @Mock + Service service; + @Test + public void test() {} + } + """ + ) + ); + } + + @Test + void addWithExistingAnnotationsWithJUnit5() { rewriteRun( //language=java java( @@ -193,7 +251,7 @@ void test() {} } @Test - void dontAddIfPresentInKotlin() { + void doNotAddIfPresentInKotlinWithJUnit5() { rewriteRun( spec -> spec.parser(KotlinParser.builder() .classpathFromResources(new InMemoryExecutionContext(), "junit-jupiter-api", "mockito-junit-jupiter", "mockito-core")), @@ -219,9 +277,9 @@ fun test() {} } @Test - void addForMockKotlin() { + void addForMockKotlinWithJUnit5() { rewriteRun( - //language=java + //language=kotlin kotlin( """ import org.junit.jupiter.api.Test