Skip to content

Commit d4b1c86

Browse files
bmuschkotimtebeek
andauthored
Add RemoveDoNothingForDefaultMocks recipe (#947)
* Add RemoveDoNothingForDefaultMocks recipe (#946) Remove unnecessary `doNothing()` stubbings on `@Mock` fields that cause UnnecessaryStubbingException after migrating to Mockito 3+. * Remove redundant `false` from MethodMatcher constructors `false` is the default for `matchOverrides`, so the single-arg constructor is sufficient and consistent with the rest of the codebase. --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 130d158 commit d4b1c86

4 files changed

Lines changed: 421 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2026 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 lombok.Getter;
19+
import org.jspecify.annotations.Nullable;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Preconditions;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.java.AnnotationMatcher;
25+
import org.openrewrite.java.JavaIsoVisitor;
26+
import org.openrewrite.java.MethodMatcher;
27+
import org.openrewrite.java.search.UsesMethod;
28+
import org.openrewrite.java.tree.J;
29+
import org.openrewrite.java.tree.Statement;
30+
31+
import java.util.HashSet;
32+
import java.util.Set;
33+
34+
public class RemoveDoNothingForDefaultMocks extends Recipe {
35+
36+
@Getter
37+
final String displayName = "Remove `doNothing()` for void methods on `@Mock` fields";
38+
39+
@Getter
40+
final String description = "Remove unnecessary `doNothing()` stubbings for void methods on `@Mock` fields. " +
41+
"Mockito mocks already do nothing for void methods by default, making these stubbings redundant " +
42+
"and triggering strict stubbing violations in Mockito 3+.";
43+
44+
private static final MethodMatcher DO_NOTHING_MATCHER = new MethodMatcher("org.mockito.Mockito doNothing()");
45+
private static final MethodMatcher STUBBER_WHEN_MATCHER = new MethodMatcher("org.mockito.stubbing.Stubber when(..)");
46+
private static final AnnotationMatcher MOCK_ANNOTATION_MATCHER = new AnnotationMatcher("@org.mockito.Mock");
47+
48+
@Override
49+
public TreeVisitor<?, ExecutionContext> getVisitor() {
50+
return Preconditions.check(
51+
new UsesMethod<>(DO_NOTHING_MATCHER),
52+
new JavaIsoVisitor<ExecutionContext>() {
53+
@Override
54+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
55+
Set<String> mockFieldNames = new HashSet<>();
56+
for (Statement stmt : classDecl.getBody().getStatements()) {
57+
if (stmt instanceof J.VariableDeclarations) {
58+
J.VariableDeclarations vd = (J.VariableDeclarations) stmt;
59+
if (vd.getLeadingAnnotations().stream().anyMatch(MOCK_ANNOTATION_MATCHER::matches)) {
60+
for (J.VariableDeclarations.NamedVariable var : vd.getVariables()) {
61+
mockFieldNames.add(var.getSimpleName());
62+
}
63+
}
64+
}
65+
}
66+
getCursor().putMessage("mockFieldNames", mockFieldNames);
67+
return super.visitClassDeclaration(classDecl, ctx);
68+
}
69+
70+
@Override
71+
public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
72+
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
73+
if (mi != null && isDoNothingOnMockField(mi)) {
74+
maybeRemoveImport("org.mockito.Mockito.doNothing");
75+
return null;
76+
}
77+
return mi;
78+
}
79+
80+
private boolean isDoNothingOnMockField(J.MethodInvocation mi) {
81+
// Pattern: doNothing().when(mock).someVoidMethod(args)
82+
if (!(mi.getSelect() instanceof J.MethodInvocation)) {
83+
return false;
84+
}
85+
J.MethodInvocation whenCall = (J.MethodInvocation) mi.getSelect();
86+
if (!STUBBER_WHEN_MATCHER.matches(whenCall)) {
87+
return false;
88+
}
89+
// Ensure doNothing() is standalone (not chained after doThrow() etc.)
90+
if (!(whenCall.getSelect() instanceof J.MethodInvocation)) {
91+
return false;
92+
}
93+
J.MethodInvocation doNothingCall = (J.MethodInvocation) whenCall.getSelect();
94+
if (!DO_NOTHING_MATCHER.matches(doNothingCall) || doNothingCall.getSelect() != null) {
95+
return false;
96+
}
97+
// Check that the when() argument references a @Mock field
98+
if (whenCall.getArguments().isEmpty() || !(whenCall.getArguments().get(0) instanceof J.Identifier)) {
99+
return false;
100+
}
101+
String mockName = ((J.Identifier) whenCall.getArguments().get(0)).getSimpleName();
102+
Set<String> mockFieldNames = getCursor().getNearestMessage("mockFieldNames");
103+
return mockFieldNames != null && mockFieldNames.contains(mockName);
104+
}
105+
}
106+
);
107+
}
108+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ recipeList:
2929
- org.openrewrite.java.RemoveAnnotation:
3030
annotationPattern: "@org.mockito.junit.jupiter.MockitoSettings(strictness=org.mockito.quality.Strictness.WARN)"
3131
- org.openrewrite.java.testing.mockito.RemoveTimesZeroAndOne
32+
- org.openrewrite.java.testing.mockito.RemoveDoNothingForDefaultMocks
3233
- org.openrewrite.java.testing.mockito.SimplifyMockitoVerifyWhenGiven
3334
- org.openrewrite.java.testing.mockito.MockConstructionToTryWithResources
3435
---

src/main/resources/META-INF/rewrite/recipes.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.tes
200200
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockitoWhenNewToMockito,Replace `PowerMockito.whenNew` with Mockito counterpart,Replaces `PowerMockito.whenNew` calls with respective `Mockito.whenConstructed` calls.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
201201
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemoveInitMocksIfRunnersSpecified,Remove `MockitoAnnotations.initMocks(this)` and `openMocks(this)` if JUnit runners specified,Remove `MockitoAnnotations.initMocks(this)` and `MockitoAnnotations.openMocks(this)` if class-level JUnit runners `@RunWith(MockitoJUnitRunner.class)` or `@ExtendWith(MockitoExtension.class)` are specified. These manual initialization calls are redundant when using Mockito's JUnit integration.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
202202
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemovePowerMockClassExtensions,Remove PowerMock class extensions,"Removes `extends PowerMockConfiguration` and `extends PowerMockTestCase` from test classes, as these are PowerMock-specific base classes not needed with Mockito.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
203+
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemoveDoNothingForDefaultMocks,Remove `doNothing()` for void methods on `@Mock` fields,Remove unnecessary `doNothing()` stubbings for void methods on `@Mock` fields. Mockito mocks already do nothing for void methods by default making these stubbings redundant and triggering strict stubbing violations in Mockito 3+.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
203204
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.RemoveTimesZeroAndOne,Remove `Mockito.times(0)` and `Mockito.times(1)`,Remove `Mockito.times(0)` and `Mockito.times(1)` from `Mockito.verify()` calls.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
204205
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.ReplaceInitMockToOpenMock,Replace `MockitoAnnotations.initMocks(this)` to `MockitoAnnotations.openMocks(this)`,Replace `MockitoAnnotations.initMocks(this)` to `MockitoAnnotations.openMocks(this)` and generate `AutoCloseable` mocks.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
205206
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.ReplaceMockitoTestExecutionListener,Replace `MockitoTestExecutionListener` with the equivalent Mockito test initialization,"Replace `@TestExecutionListeners(MockitoTestExecutionListener.class)` with the appropriate Mockito initialization for the test framework in use: `@ExtendWith(MockitoExtension.class)` for JUnit 5, `@RunWith(MockitoJUnitRunner.class)` for JUnit 4, or `MockitoAnnotations.openMocks(this)` for TestNG.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""targetFramework"",""type"":""String"",""displayName"":""Target framework"",""description"":""The test framework to use when imports alone cannot determine the framework. Typically set by wrapper recipes that check project dependencies."",""valid"":[""jupiter"",""junit4"",""testng""]}]",

0 commit comments

Comments
 (0)