Skip to content

Commit 4f669aa

Browse files
arodionovjevanlingengithub-actions[bot]timtebeek
authored
Recipe to replace initMocks with openMocks (#700)
* Recipe to replace initMocks to openMocks * - trying to use doAfterVisit * Return the `J.MethodDeclaration` immediately instead of returning it as statement * - fix tests * - append @ AfterEach method body if exists * - generate variable name * - added tests * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * - Move `mocks` variable generation to first visitor, so the afterVisitor is aware of this variable - Add extra check when adding the `mocks.close()` call in the `@AfterEach` to prevent it from applying multiple times - Remove unneeded contextSensitive() calls * Remove double call to `visitClassDeclaration` * - added test with inner class * - fix cas with inner class * - updated mockito.yml * - updated JunitMockitoUpgradeIntegrationTest * - not apply recipe for jUnit4 * No need for `updateCursor` here * Drop escape hatches from `replaceMockAnnotation` to show failure in CI * Use deliberate tree traversal & messaging in first pass visitor * Drop unnecessary `.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx))` * Remove unused import * - adjusted Mockito version to the particular test * Add examples.yml --------- Co-authored-by: lingenj <[email protected]> Co-authored-by: Jacob van Lingen <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <[email protected]>
1 parent 247f25b commit 4f669aa

File tree

6 files changed

+672
-5
lines changed

6 files changed

+672
-5
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.testing.mockito;
17+
18+
import org.openrewrite.ExecutionContext;
19+
import org.openrewrite.Preconditions;
20+
import org.openrewrite.Recipe;
21+
import org.openrewrite.TreeVisitor;
22+
import org.openrewrite.java.*;
23+
import org.openrewrite.java.search.UsesMethod;
24+
import org.openrewrite.java.search.UsesType;
25+
import org.openrewrite.java.service.AnnotationService;
26+
import org.openrewrite.java.tree.J;
27+
import org.openrewrite.java.tree.Statement;
28+
29+
import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
30+
import static org.openrewrite.java.VariableNameUtils.generateVariableName;
31+
32+
public class ReplaceInitMockToOpenMock extends Recipe {
33+
34+
@Override
35+
public String getDisplayName() {
36+
return "Replace `MockitoAnnotations.initMocks(this)` to `MockitoAnnotations.openMocks(this)`";
37+
}
38+
39+
@Override
40+
public String getDescription() {
41+
return "Replace `MockitoAnnotations.initMocks(this)` to `MockitoAnnotations.openMocks(this)` and generate `AutoCloseable` mocks.";
42+
}
43+
44+
private static final String MOCKITO_EXTENSION = "org.mockito.junit.jupiter.MockitoExtension";
45+
private static final String MOCKITO_JUNIT_RUNNER = "org.mockito.junit.MockitoJUnitRunner";
46+
private static final String JUPITER_BEFORE_EACH = "org.junit.jupiter.api.BeforeEach";
47+
private static final AnnotationMatcher BEFORE_EACH_MATCHER = new AnnotationMatcher("@" + JUPITER_BEFORE_EACH);
48+
private static final AnnotationMatcher AFTER_EACH_MATCHER = new AnnotationMatcher("@org.junit.jupiter.api.AfterEach");
49+
private static final MethodMatcher INIT_MOCKS_MATCHER = new MethodMatcher("org.mockito.MockitoAnnotations initMocks(..)", false);
50+
51+
@Override
52+
public TreeVisitor<?, ExecutionContext> getVisitor() {
53+
TreeVisitor<?, ExecutionContext> preconditions = Preconditions.and(
54+
new UsesMethod<>(INIT_MOCKS_MATCHER),
55+
new UsesType<>(JUPITER_BEFORE_EACH, false),
56+
Preconditions.not(new UsesType<>(MOCKITO_EXTENSION, false)),
57+
Preconditions.not(new UsesType<>(MOCKITO_JUNIT_RUNNER, false))
58+
);
59+
return Preconditions.check(preconditions, new JavaIsoVisitor<ExecutionContext>() {
60+
private String variableName = "mocks";
61+
62+
@Override
63+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
64+
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
65+
if (getCursor().getMessage("initMocksFound", false)) {
66+
variableName = generateVariableName("mocks", getCursor(), INCREMENT_NUMBER);
67+
J.ClassDeclaration after = JavaTemplate.apply("private AutoCloseable " + variableName + ";",
68+
getCursor(), cd.getBody().getCoordinates().firstStatement());
69+
return maybeAutoFormat(cd, after, ctx);
70+
}
71+
return cd;
72+
}
73+
74+
@Override
75+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
76+
if (service(AnnotationService.class).matches(getCursor(), BEFORE_EACH_MATCHER)) {
77+
return super.visitMethodDeclaration(method, ctx);
78+
}
79+
return method;
80+
}
81+
82+
@Override
83+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
84+
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
85+
if (INIT_MOCKS_MATCHER.matches(mi)) {
86+
getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, "initMocksFound", true);
87+
doAfterVisit(updateJUnitLifecycleMethods);
88+
}
89+
return mi;
90+
}
91+
92+
final TreeVisitor<J, ExecutionContext> updateJUnitLifecycleMethods = new JavaIsoVisitor<ExecutionContext>() {
93+
94+
private boolean isAnnotatedMethodPresent(J.ClassDeclaration cd, AnnotationMatcher beforeEachMatcher) {
95+
return cd.getBody().getStatements().stream().anyMatch(
96+
st -> st instanceof J.MethodDeclaration &&
97+
((J.MethodDeclaration) st).getLeadingAnnotations().stream().anyMatch(beforeEachMatcher::matches)
98+
);
99+
}
100+
101+
@Override
102+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext ctx) {
103+
if (!isAnnotatedMethodPresent(cd, AFTER_EACH_MATCHER) && isAnnotatedMethodPresent(cd, BEFORE_EACH_MATCHER)) {
104+
maybeAddImport("org.junit.jupiter.api.AfterEach");
105+
cd = JavaTemplate.builder("@AfterEach\nvoid tearDown() throws Exception {\n}")
106+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-jupiter-api-5"))
107+
.imports("org.junit.jupiter.api.AfterEach")
108+
.build()
109+
.apply(getCursor(), cd.getBody().getCoordinates().lastStatement());
110+
}
111+
112+
cd = super.visitClassDeclaration(cd, ctx);
113+
return autoFormat(cd, ctx);
114+
}
115+
116+
@Override
117+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
118+
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
119+
120+
if (service(AnnotationService.class).matches(getCursor(), BEFORE_EACH_MATCHER) && md.getBody() != null) {
121+
maybeRemoveImport("org.mockito.MockitoAnnotations.initMocks");
122+
return (J.MethodDeclaration) new JavaVisitor<ExecutionContext>() {
123+
@Override
124+
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
125+
J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
126+
if (INIT_MOCKS_MATCHER.matches(mi)) {
127+
return JavaTemplate.builder(variableName + " = MockitoAnnotations.openMocks(this);")
128+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core"))
129+
.imports("org.mockito.MockitoAnnotations")
130+
.contextSensitive()
131+
.build()
132+
.apply(getCursor(), mi.getCoordinates().replace());
133+
}
134+
return mi;
135+
}
136+
}.visitNonNull(md, ctx, getCursor().getParentOrThrow());
137+
} else if (service(AnnotationService.class).matches(getCursor(), AFTER_EACH_MATCHER) && md.getBody() != null) {
138+
for (Statement st : md.getBody().getStatements()) {
139+
if (st instanceof J.MethodInvocation &&
140+
((J.MethodInvocation) st).getSelect() instanceof J.Identifier &&
141+
((J.Identifier) ((J.MethodInvocation) st).getSelect()).getSimpleName().equals(variableName)) {
142+
return md;
143+
}
144+
}
145+
146+
md = JavaTemplate.builder(variableName + ".close();")
147+
.contextSensitive()
148+
.build()
149+
.apply(getCursor(), md.getBody().getCoordinates().lastStatement());
150+
return maybeAutoFormat(method, md, ctx);
151+
}
152+
153+
return md;
154+
}
155+
};
156+
}
157+
);
158+
}
159+
}

src/main/resources/META-INF/rewrite/examples.yml

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2482,6 +2482,7 @@ examples:
24822482
after: |
24832483
package org.openrewrite.java.testing.junit5;
24842484
2485+
import org.junit.jupiter.api.AfterEach;
24852486
import org.junit.jupiter.api.BeforeEach;
24862487
import org.junit.jupiter.api.Test;
24872488
import org.mockito.Mock;
@@ -2492,12 +2493,13 @@ examples:
24922493
import static org.mockito.Mockito.verify;
24932494
24942495
public class MockitoTests {
2496+
private AutoCloseable mocks;
24952497
@Mock
24962498
List<String> mockedList;
24972499
24982500
@BeforeEach
24992501
public void initMocks() {
2500-
MockitoAnnotations.initMocks(this);
2502+
mocks = MockitoAnnotations.openMocks(this);
25012503
}
25022504
25032505
@Test
@@ -2509,6 +2511,11 @@ examples:
25092511
verify(mockedList).add("one");
25102512
verify(mockedList).clear();
25112513
}
2514+
2515+
@AfterEach
2516+
void tearDown() throws Exception {
2517+
mocks.close();
2518+
}
25122519
}
25132520
language: java
25142521
---
@@ -3587,6 +3594,59 @@ examples:
35873594
language: java
35883595
---
35893596
type: specs.openrewrite.org/v1beta/example
3597+
recipeName: org.openrewrite.java.testing.mockito.ReplaceInitMockToOpenMock
3598+
examples:
3599+
- description: ''
3600+
sources:
3601+
- before: |
3602+
import org.mockito.MockitoAnnotations;
3603+
import org.junit.jupiter.api.BeforeEach;
3604+
3605+
class A {
3606+
3607+
@BeforeEach
3608+
public void setUp() {
3609+
test1();
3610+
MockitoAnnotations.initMocks(this);
3611+
test2();
3612+
}
3613+
3614+
public void test1() {
3615+
}
3616+
3617+
public void test2() {
3618+
}
3619+
}
3620+
after: |
3621+
import org.mockito.MockitoAnnotations;
3622+
import org.junit.jupiter.api.AfterEach;
3623+
import org.junit.jupiter.api.BeforeEach;
3624+
3625+
class A {
3626+
3627+
private AutoCloseable mocks;
3628+
3629+
@BeforeEach
3630+
public void setUp() {
3631+
test1();
3632+
mocks = MockitoAnnotations.openMocks(this);
3633+
test2();
3634+
}
3635+
3636+
public void test1() {
3637+
}
3638+
3639+
public void test2() {
3640+
}
3641+
3642+
@AfterEach
3643+
void tearDown() throws Exception {
3644+
mocks.close();
3645+
}
3646+
}
3647+
language: java
3648+
---
3649+
type: specs.openrewrite.org/v1beta/example
35903650
recipeName: org.openrewrite.java.testing.mockito.ReplacePowerMockito
35913651
examples:
35923652
- description: ''

src/main/resources/META-INF/rewrite/junit5.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ recipeList:
7474
- org.openrewrite.java.testing.junit5.AddHamcrestJUnitDependency
7575
- org.openrewrite.java.testing.junit5.UseHamcrestAssertThat
7676
- org.openrewrite.java.testing.junit5.MigrateAssumptions
77-
- org.openrewrite.java.testing.junit5.UseMockitoExtension
7877
- org.openrewrite.java.testing.junit5.UseTestMethodOrder
7978
- org.openrewrite.java.testing.junit5.MigrateJUnitTestCase
8079
- org.openrewrite.java.ChangeMethodName:
@@ -97,6 +96,7 @@ recipeList:
9796
- org.openrewrite.java.testing.junit5.VertxUnitToVertxJunit5
9897
- org.openrewrite.java.testing.junit5.EnclosedToNested
9998
- org.openrewrite.java.testing.junit5.AddMissingNested
99+
- org.openrewrite.java.testing.junit5.UseMockitoExtension
100100
- org.openrewrite.java.testing.hamcrest.AddHamcrestIfUsed
101101
- org.openrewrite.java.testing.junit5.UseXMLUnitLegacy
102102
- org.openrewrite.java.dependencies.RemoveDependency:

src/main/resources/META-INF/rewrite/mockito.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ recipeList:
8181
groupId: net.bytebuddy
8282
artifactId: byte-buddy*
8383
newVersion: 1.12.19
84+
- org.openrewrite.java.testing.mockito.ReplaceInitMockToOpenMock
8485
---
8586
type: specs.openrewrite.org/v1beta/recipe
8687
name: org.openrewrite.java.testing.mockito.Mockito1to3Migration

src/test/java/org/openrewrite/java/testing/mockito/JunitMockitoUpgradeIntegrationTest.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.openrewrite.java.JavaParser;
2323
import org.openrewrite.test.RecipeSpec;
2424
import org.openrewrite.test.RewriteTest;
25-
import org.openrewrite.test.TypeValidation;
2625

2726
import static org.openrewrite.java.Assertions.java;
2827

@@ -51,7 +50,9 @@ public void defaults(RecipeSpec spec) {
5150
void replaceMockAnnotation() {
5251
//language=java
5352
rewriteRun(
54-
spec -> spec.typeValidationOptions(TypeValidation.none()),
53+
spec -> spec
54+
.parser(JavaParser.fromJavaVersion()
55+
.classpathFromResources(new InMemoryExecutionContext(), "mockito-core", "junit-4", "hamcrest-3", "junit-jupiter-api-5")),
5556
java(
5657
"""
5758
package org.openrewrite.java.testing.junit5;
@@ -88,6 +89,7 @@ public void usingAnnotationBasedMock() {
8889
"""
8990
package org.openrewrite.java.testing.junit5;
9091
92+
import org.junit.jupiter.api.AfterEach;
9193
import org.junit.jupiter.api.BeforeEach;
9294
import org.junit.jupiter.api.Test;
9395
import org.mockito.Mock;
@@ -98,12 +100,13 @@ public void usingAnnotationBasedMock() {
98100
import static org.mockito.Mockito.verify;
99101
100102
public class MockitoTests {
103+
private AutoCloseable mocks;
101104
@Mock
102105
List<String> mockedList;
103106
104107
@BeforeEach
105108
public void initMocks() {
106-
MockitoAnnotations.initMocks(this);
109+
mocks = MockitoAnnotations.openMocks(this);
107110
}
108111
109112
@Test
@@ -115,6 +118,11 @@ public void usingAnnotationBasedMock() {
115118
verify(mockedList).add("one");
116119
verify(mockedList).clear();
117120
}
121+
122+
@AfterEach
123+
void tearDown() throws Exception {
124+
mocks.close();
125+
}
118126
}
119127
"""
120128
)

0 commit comments

Comments
 (0)