Skip to content

Commit cf78c5a

Browse files
Enhance RemoveInitMocksIfRunnersSpecified to also remove openMocks (#904)
* Enhance RemoveInitMocksIfRunnersSpecified to also remove openMocks When `@ExtendWith(MockitoExtension.class)` or `@RunWith(MockitoJUnitRunner.class)` is present, this recipe now removes both: - `MockitoAnnotations.initMocks(this)` - `MockitoAnnotations.openMocks(this)` For the openMocks case, the recipe also handles: - Field declarations (e.g., `private AutoCloseable mocks`) - Assignment statements (e.g., `mocks = MockitoAnnotations.openMocks(this)`) - close() calls on the AutoCloseable field - Empty @AfterEach/@after methods after removing close() calls Closes moderneinc/customer-requests#1775 * rework with better two-pass pattern * only remove an empty method if it is annotated with Before* or After* * remove unused imports and dead code * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * address review comment --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 75c12cd commit cf78c5a

File tree

2 files changed

+269
-27
lines changed

2 files changed

+269
-27
lines changed

src/main/java/org/openrewrite/java/testing/mockito/RemoveInitMocksIfRunnersSpecified.java

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,65 +24,156 @@
2424
import org.openrewrite.java.AnnotationMatcher;
2525
import org.openrewrite.java.JavaIsoVisitor;
2626
import org.openrewrite.java.MethodMatcher;
27+
import org.openrewrite.java.search.SemanticallyEqual;
2728
import org.openrewrite.java.search.UsesMethod;
2829
import org.openrewrite.java.search.UsesType;
2930
import org.openrewrite.java.service.AnnotationService;
31+
import org.openrewrite.java.tree.Expression;
3032
import org.openrewrite.java.tree.J;
3133

34+
import java.util.Arrays;
35+
import java.util.HashSet;
36+
import java.util.List;
37+
import java.util.Set;
38+
3239
public class RemoveInitMocksIfRunnersSpecified extends Recipe {
3340

3441
@Getter
35-
final String displayName = "Remove `MockitoAnnotations.initMocks(this)` if specified JUnit runners";
42+
final String displayName = "Remove `MockitoAnnotations.initMocks(this)` and `openMocks(this)` if JUnit runners specified";
3643

3744
@Getter
38-
final String description = "Remove `MockitoAnnotations.initMocks(this)` if specified class-level JUnit runners `@RunWith(MockitoJUnitRunner.class)` or `@ExtendWith(MockitoExtension.class)`.";
45+
final String description = "Remove `MockitoAnnotations.initMocks(this)` and `MockitoAnnotations.openMocks(this)` if class-level " +
46+
"JUnit runners `@RunWith(MockitoJUnitRunner.class)` or `@ExtendWith(MockitoExtension.class)` are specified. " +
47+
"These manual initialization calls are redundant when using Mockito's JUnit integration.";
3948

4049
private static final String MOCKITO_EXTENSION = "org.mockito.junit.jupiter.MockitoExtension";
4150
private static final String MOCKITO_JUNIT_RUNNER = "org.mockito.junit.MockitoJUnitRunner";
4251
private static final AnnotationMatcher MOCKITO_EXTENSION_MATCHER = new AnnotationMatcher("@org.junit.jupiter.api.extension.ExtendWith(" + MOCKITO_EXTENSION + ".class)");
4352
private static final AnnotationMatcher MOCKITO_JUNIT_MATCHER = new AnnotationMatcher("@org.junit.runner.RunWith(" + MOCKITO_JUNIT_RUNNER + ".class)");
4453
private static final MethodMatcher INIT_MOCKS_MATCHER = new MethodMatcher("org.mockito.MockitoAnnotations initMocks(..)", false);
54+
private static final MethodMatcher OPEN_MOCKS_MATCHER = new MethodMatcher("org.mockito.MockitoAnnotations openMocks(..)", false);
55+
private static final MethodMatcher CLOSEABLE_MATCHER = new MethodMatcher("java.lang.AutoCloseable close()", false);
56+
private static List<AnnotationMatcher> BEFORE_AND_AFTER_MATCHERS = Arrays.asList(
57+
new AnnotationMatcher("@org.junit.jupiter.api.BeforeAll"),
58+
new AnnotationMatcher("@org.junit.jupiter.api.BeforeEach"),
59+
new AnnotationMatcher("@org.junit.BeforeClass"),
60+
new AnnotationMatcher("@org.junit.Before"),
61+
new AnnotationMatcher("@org.junit.jupiter.api.AfterAll"),
62+
new AnnotationMatcher("@org.junit.jupiter.api.AfterEach"),
63+
new AnnotationMatcher("@org.junit.AfterClass"),
64+
new AnnotationMatcher("@org.junit.After")
65+
);
4566

4667
@Override
4768
public TreeVisitor<?, ExecutionContext> getVisitor() {
4869
return Preconditions.check(
4970
Preconditions.and(
50-
new UsesMethod<>(INIT_MOCKS_MATCHER),
71+
Preconditions.or(
72+
new UsesMethod<>(INIT_MOCKS_MATCHER),
73+
new UsesMethod<>(OPEN_MOCKS_MATCHER)
74+
),
5175
Preconditions.or(
5276
new UsesType<>(MOCKITO_EXTENSION, false),
5377
new UsesType<>(MOCKITO_JUNIT_RUNNER, false)
5478
)
5579
),
5680
new JavaIsoVisitor<ExecutionContext>() {
57-
5881
@Override
59-
public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
60-
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
61-
if (INIT_MOCKS_MATCHER.matches(mi)) {
62-
maybeRemoveImport("org.mockito.MockitoAnnotations");
63-
return null;
64-
}
65-
return mi;
82+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
83+
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
84+
85+
Set<Expression> closeables = new JavaIsoVisitor<Set<Expression>>() {
86+
@Override
87+
public J.Assignment visitAssignment(J.Assignment assignment, Set<Expression> exprSet) {
88+
J.Assignment as = super.visitAssignment(assignment, exprSet);
89+
90+
if (isMockitoOpenMocksCall(assignment.getAssignment())) {
91+
exprSet.add(assignment.getVariable());
92+
}
93+
return as;
94+
}
95+
}.reduce(cd, new HashSet<>());
96+
97+
J.ClassDeclaration modifiedCd = (J.ClassDeclaration) new JavaIsoVisitor<ExecutionContext>() {
98+
99+
@Override
100+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
101+
if (service(AnnotationService.class).matches(getCursor(), MOCKITO_EXTENSION_MATCHER) ||
102+
service(AnnotationService.class).matches(getCursor(), MOCKITO_JUNIT_MATCHER)) {
103+
return super.visitClassDeclaration(classDecl, ctx);
104+
}
105+
return classDecl;
106+
}
107+
108+
@Override
109+
public J.@Nullable Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) {
110+
J.Assignment a = super.visitAssignment(assignment, ctx);
111+
// Remove assignments where RHS is initMocks/openMocks
112+
if (isMockitoInitMocksCall(assignment.getAssignment()) || isMockitoOpenMocksCall(assignment.getAssignment())) {
113+
maybeRemoveImport("org.mockito.MockitoAnnotations");
114+
return null;
115+
}
116+
return a;
117+
}
118+
119+
@Override
120+
public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
121+
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
122+
if (OPEN_MOCKS_MATCHER.matches(mi) || INIT_MOCKS_MATCHER.matches(mi)) {
123+
return null;
124+
}
125+
if (CLOSEABLE_MATCHER.matches(mi) && mi.getSelect() != null && closeables.stream().anyMatch(it -> SemanticallyEqual.areEqual(it, mi.getSelect()))) {
126+
return null;
127+
}
128+
return mi;
129+
}
130+
131+
@Override
132+
public J.@Nullable VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
133+
J.VariableDeclarations vd = super.visitVariableDeclarations(multiVariable, ctx);
134+
// Remove field declarations for fields that store openMocks result
135+
for (J.VariableDeclarations.NamedVariable variable : vd.getVariables()) {
136+
if (closeables.stream().anyMatch(it -> SemanticallyEqual.areEqual(it, variable.getDeclarator()))) {
137+
return null;
138+
}
139+
}
140+
return vd;
141+
}
142+
143+
@Override
144+
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
145+
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
146+
if (md != method && md.getBody() != null && md.getBody().getStatements().isEmpty()) {
147+
// Only remove empty Before and After methods
148+
if (BEFORE_AND_AFTER_MATCHERS.stream().anyMatch(matcher ->
149+
service(AnnotationService.class).matches(getCursor(), matcher))) {
150+
return null;
151+
}
152+
}
153+
return md;
154+
}
155+
156+
}.visitNonNull(cd, ctx, getCursor().getParentOrThrow());
157+
158+
maybeRemoveImport("org.mockito.MockitoAnnotations");
159+
maybeRemoveImport("org.junit.jupiter.api.BeforeAll");
160+
maybeRemoveImport("org.junit.jupiter.api.BeforeEach");
161+
maybeRemoveImport("org.junit.jupiter.api.AfterAll");
162+
maybeRemoveImport("org.junit.jupiter.api.AfterEach");
163+
maybeRemoveImport("org.junit.BeforeClass");
164+
maybeRemoveImport("org.junit.Before");
165+
maybeRemoveImport("org.junit.AfterClass");
166+
maybeRemoveImport("org.junit.After");
167+
168+
return modifiedCd;
66169
}
67170

68-
@Override
69-
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
70-
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
71-
if (md != method && md.getBody() != null && md.getBody().getStatements().isEmpty()) {
72-
maybeRemoveImport("org.junit.jupiter.api.BeforeEach");
73-
maybeRemoveImport("org.junit.Before");
74-
return null;
75-
}
76-
return md;
171+
private boolean isMockitoOpenMocksCall(Expression expr) {
172+
return expr instanceof J.MethodInvocation && OPEN_MOCKS_MATCHER.matches((J.MethodInvocation)expr);
77173
}
78174

79-
@Override
80-
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext ctx) {
81-
if (service(AnnotationService.class).matches(updateCursor(cd), MOCKITO_EXTENSION_MATCHER) ||
82-
service(AnnotationService.class).matches(updateCursor(cd), MOCKITO_JUNIT_MATCHER)) {
83-
return super.visitClassDeclaration(cd, ctx);
84-
}
85-
return cd;
175+
private boolean isMockitoInitMocksCall(Expression expr) {
176+
return expr instanceof J.MethodInvocation && INIT_MOCKS_MATCHER.matches((J.MethodInvocation)expr);
86177
}
87178
}
88179
);

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

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ public void test() {
101101
@ExtendWith(MockitoExtension.class)
102102
class A {
103103
104+
public void setUp() {
105+
}
106+
104107
public void test() {
105108
}
106109
}
@@ -199,4 +202,152 @@ public void test() {
199202
)
200203
);
201204
}
205+
206+
@Test
207+
void removeOpenMocksInJUnit5() {
208+
rewriteRun(
209+
//language=java
210+
java(
211+
"""
212+
import org.junit.jupiter.api.BeforeEach;
213+
import org.junit.jupiter.api.extension.ExtendWith;
214+
import org.mockito.junit.jupiter.MockitoExtension;
215+
import org.mockito.MockitoAnnotations;
216+
217+
@ExtendWith(MockitoExtension.class)
218+
class A {
219+
220+
@BeforeEach
221+
public void setUp() {
222+
MockitoAnnotations.openMocks(this);
223+
}
224+
225+
public void test() {
226+
}
227+
}
228+
""",
229+
"""
230+
import org.junit.jupiter.api.extension.ExtendWith;
231+
import org.mockito.junit.jupiter.MockitoExtension;
232+
233+
@ExtendWith(MockitoExtension.class)
234+
class A {
235+
236+
public void test() {
237+
}
238+
}
239+
"""
240+
)
241+
);
242+
}
243+
244+
@Test
245+
void removeOpenMocksWithStaticImport() {
246+
rewriteRun(
247+
//language=java
248+
java(
249+
"""
250+
import org.junit.jupiter.api.extension.ExtendWith;
251+
import org.mockito.junit.jupiter.MockitoExtension;
252+
import org.mockito.MockitoAnnotations;
253+
254+
import static org.mockito.MockitoAnnotations.openMocks;
255+
256+
@ExtendWith(MockitoExtension.class)
257+
class A {
258+
259+
public void setUp() {
260+
openMocks(this);
261+
}
262+
263+
public void test() {
264+
}
265+
}
266+
""",
267+
"""
268+
import org.junit.jupiter.api.extension.ExtendWith;
269+
import org.mockito.junit.jupiter.MockitoExtension;
270+
271+
@ExtendWith(MockitoExtension.class)
272+
class A {
273+
274+
public void setUp() {
275+
}
276+
277+
public void test() {
278+
}
279+
}
280+
"""
281+
)
282+
);
283+
}
284+
285+
@Test
286+
void notRemoveOpenMocksWithoutRunners() {
287+
rewriteRun(
288+
//language=java
289+
java(
290+
"""
291+
import org.junit.jupiter.api.extension.ExtendWith;
292+
import org.mockito.junit.jupiter.MockitoExtension;
293+
import org.mockito.MockitoAnnotations;
294+
295+
class A {
296+
297+
public void setUp() {
298+
MockitoAnnotations.openMocks(this);
299+
}
300+
301+
public void test() {
302+
}
303+
}
304+
"""
305+
)
306+
);
307+
}
308+
309+
@Test
310+
void removeOpenMocksWithFieldAndClose() {
311+
rewriteRun(
312+
//language=java
313+
java(
314+
"""
315+
import org.junit.jupiter.api.AfterEach;
316+
import org.junit.jupiter.api.BeforeEach;
317+
import org.junit.jupiter.api.extension.ExtendWith;
318+
import org.mockito.junit.jupiter.MockitoExtension;
319+
import org.mockito.MockitoAnnotations;
320+
321+
@ExtendWith(MockitoExtension.class)
322+
class A {
323+
private AutoCloseable mocks;
324+
325+
@BeforeEach
326+
public void setUp() {
327+
mocks = MockitoAnnotations.openMocks(this);
328+
}
329+
330+
@AfterEach
331+
public void tearDown() throws Exception {
332+
mocks.close();
333+
}
334+
335+
public void test() {
336+
}
337+
}
338+
""",
339+
"""
340+
import org.junit.jupiter.api.extension.ExtendWith;
341+
import org.mockito.junit.jupiter.MockitoExtension;
342+
343+
@ExtendWith(MockitoExtension.class)
344+
class A {
345+
346+
public void test() {
347+
}
348+
}
349+
"""
350+
)
351+
);
352+
}
202353
}

0 commit comments

Comments
 (0)