Skip to content

Commit 04c94f8

Browse files
bmuschkotimtebeek
andauthored
Add PowerMockitoDoStubbingToMockito recipe (#948)
* Add PowerMockitoDoStubbingToMockito recipe (#946) Rewrite PowerMockito's string-based private method stubbing into standard Mockito chained form so migrated code compiles. * Fix CSV escaping for fields containing commas and quotes * Refine PowerMockitoDoStubbingToMockito per review feedback Use UsesMethod precondition instead of UsesType for more precise matching, static imports for Collections methods, emptyList() for no-arg calls, and ListUtils.mapFirst to simplify arg remapping. --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent d4b1c86 commit 04c94f8

4 files changed

Lines changed: 342 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.internal.ListUtils;
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.Expression;
29+
import org.openrewrite.java.tree.J;
30+
import org.openrewrite.java.tree.JavaType;
31+
import org.openrewrite.java.tree.Space;
32+
import java.util.List;
33+
34+
import static java.util.Collections.emptyList;
35+
import static java.util.Collections.singletonList;
36+
37+
public class PowerMockitoDoStubbingToMockito extends Recipe {
38+
39+
private static final MethodMatcher STUBBER_WHEN_MATCHER =
40+
new MethodMatcher("org.powermock.api.mockito.expectation.PowerMockitoStubber when(..)");
41+
42+
@Getter
43+
final String displayName = "Replace PowerMockito `doX().when(instance, \"method\")` with Mockito-compatible stubbing";
44+
45+
@Getter
46+
final String description = "Replaces PowerMockito's private method stubbing pattern " +
47+
"`doNothing().when(instance, \"methodName\", args...)` with the standard Mockito pattern " +
48+
"`doNothing().when(instance).methodName(args...)`.";
49+
50+
@Override
51+
public TreeVisitor<?, ExecutionContext> getVisitor() {
52+
return Preconditions.check(
53+
new UsesMethod<>(STUBBER_WHEN_MATCHER),
54+
new JavaIsoVisitor<ExecutionContext>() {
55+
56+
@Override
57+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
58+
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
59+
60+
if (!STUBBER_WHEN_MATCHER.matches(mi)) {
61+
return mi;
62+
}
63+
64+
List<Expression> args = mi.getArguments();
65+
if (args.size() < 2) {
66+
return mi;
67+
}
68+
69+
// Second argument must be a String literal (the method name to stub)
70+
if (!(args.get(1) instanceof J.Literal) || !(((J.Literal) args.get(1)).getValue() instanceof String)) {
71+
return mi;
72+
}
73+
74+
// Skip static method variant where first arg is a Class literal
75+
Expression firstArg = args.get(0);
76+
if (firstArg instanceof J.FieldAccess && "class".equals(((J.FieldAccess) firstArg).getSimpleName())) {
77+
return mi;
78+
}
79+
80+
String targetMethodName = (String) ((J.Literal) args.get(1)).getValue();
81+
List<Expression> extraArgs = args.subList(2, args.size());
82+
83+
// Rewrite: doX().when(instance, "method", args...) → doX().when(instance).method(args...)
84+
85+
// 1. Create when(instance) - strip method name and extra args, keep just the instance
86+
// Update the when() method type: PowerMockitoStubber.when(T,String,Object...) returns void,
87+
// but Mockito's Stubber.when(T) returns T — we need the return type set to the instance type
88+
// so the chained method call resolves correctly.
89+
JavaType instanceType = firstArg.getType();
90+
JavaType.Method originalWhenType = mi.getMethodType();
91+
JavaType.Method updatedWhenType = null;
92+
if (originalWhenType != null && instanceType != null) {
93+
updatedWhenType = originalWhenType
94+
.withReturnType(instanceType)
95+
.withParameterTypes(singletonList(instanceType))
96+
.withParameterNames(singletonList("mock"));
97+
}
98+
J.MethodInvocation whenWithInstance = mi
99+
.withPrefix(Space.EMPTY)
100+
.withArguments(singletonList(firstArg.withPrefix(Space.EMPTY)))
101+
.withMethodType(updatedWhenType)
102+
.withName(mi.getName().withType(updatedWhenType));
103+
104+
// 2. Build .method(args...) chained on when(instance)
105+
List<Expression> newArgs = extraArgs.isEmpty() ?
106+
emptyList() :
107+
ListUtils.mapFirst(extraArgs, a -> a.withPrefix(Space.EMPTY));
108+
109+
JavaType.Method resolvedMethodType = resolveTargetMethod(
110+
firstArg.getType(), targetMethodName, extraArgs.size());
111+
112+
J.Identifier newName = mi.getName()
113+
.withSimpleName(targetMethodName)
114+
.withType(resolvedMethodType);
115+
116+
return mi
117+
.withSelect(whenWithInstance)
118+
.withName(newName)
119+
.withMethodType(resolvedMethodType)
120+
.withArguments(newArgs);
121+
}
122+
123+
/**
124+
* Resolve the target method from the instance type and the method name.
125+
* Returns null if the method cannot be unambiguously resolved.
126+
*/
127+
private JavaType.@Nullable Method resolveTargetMethod(
128+
@Nullable JavaType targetType, String methodName, int expectedParamCount) {
129+
if (!(targetType instanceof JavaType.FullyQualified)) {
130+
return null;
131+
}
132+
JavaType.Method match = null;
133+
for (JavaType.FullyQualified current = (JavaType.FullyQualified) targetType;
134+
current != null; current = current.getSupertype()) {
135+
for (JavaType.Method method : current.getMethods()) {
136+
if (method.getName().equals(methodName) &&
137+
method.getParameterTypes().size() == expectedParamCount) {
138+
if (match != null) {
139+
return null; // ambiguous overload
140+
}
141+
match = method;
142+
}
143+
}
144+
}
145+
return match;
146+
}
147+
}
148+
);
149+
}
150+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ recipeList:
3232
methodPattern: org.powermock.api.mockito.PowerMockito mockStatic(..)
3333
fullyQualifiedTargetTypeName: org.mockito.Mockito
3434
returnType: org.mockito.MockedStatic
35+
- org.openrewrite.java.testing.mockito.PowerMockitoDoStubbingToMockito
3536
- org.openrewrite.java.ChangeMethodTargetToStatic:
3637
methodPattern: org.powermock.api.mockito.PowerMockito do*(..)
3738
fullyQualifiedTargetTypeName: org.mockito.Mockito

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.tes
197197
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockRunnerDelegateToRunWith,Replace PowerMock runner with standard `@RunWith`,"Replaces `@RunWith(PowerMockRunner.class)`. If `@PowerMockRunnerDelegate(X.class)` is present, promotes the delegate runner to `@RunWith(X.class)`. Otherwise, removes the `@RunWith(PowerMockRunner.class)` annotation entirely.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
198198
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockWhiteboxToJavaReflection,Replace PowerMock `Whitebox` with Java reflection,"Replace `org.powermock.reflect.Whitebox` calls (`setInternalState`, `getInternalState`, `invokeMethod`) with plain Java reflection using `java.lang.reflect.Field` and `java.lang.reflect.Method`.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
199199
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockitoMockStaticToMockito,Replace `PowerMock.mockStatic()` with `Mockito.mockStatic()`,Replaces `PowerMockito.mockStatic()` by `Mockito.mockStatic()`. Removes the `@PrepareForTest` annotation.,1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
200+
maven,org.openrewrite.recipe:rewrite-testing-frameworks,org.openrewrite.java.testing.mockito.PowerMockitoDoStubbingToMockito,"Replace PowerMockito `doX().when(instance, ""method"")` with Mockito-compatible stubbing","Replaces PowerMockito's private method stubbing pattern `doNothing().when(instance, ""methodName"", args...)` with the standard Mockito pattern `doNothing().when(instance).methodName(args...)`.",1,Mockito,Testing,Java,,,Basic building blocks for transforming Java code.,,
200201
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.,,
201202
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.,,
202203
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.,,
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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 org.junit.jupiter.api.Test;
19+
import org.openrewrite.DocumentExample;
20+
import org.openrewrite.InMemoryExecutionContext;
21+
import org.openrewrite.java.JavaParser;
22+
import org.openrewrite.test.RecipeSpec;
23+
import org.openrewrite.test.RewriteTest;
24+
import org.openrewrite.test.TypeValidation;
25+
26+
import static org.openrewrite.java.Assertions.java;
27+
28+
class PowerMockitoDoStubbingToMockitoTest implements RewriteTest {
29+
@Override
30+
public void defaults(RecipeSpec spec) {
31+
spec
32+
.parser(JavaParser.fromJavaVersion()
33+
.classpathFromResources(new InMemoryExecutionContext(),
34+
"mockito-core-3.12",
35+
"junit-jupiter-api-5",
36+
"powermock-core-1",
37+
"powermock-api-mockito-1"
38+
))
39+
.recipe(new PowerMockitoDoStubbingToMockito())
40+
.typeValidationOptions(TypeValidation.builder()
41+
.identifiers(false)
42+
.build());
43+
}
44+
45+
@DocumentExample
46+
@Test
47+
void doNothingWithStringMethodName() {
48+
//language=java
49+
rewriteRun(
50+
java(
51+
"""
52+
import org.junit.jupiter.api.BeforeEach;
53+
import org.powermock.api.mockito.PowerMockito;
54+
import java.util.Calendar;
55+
56+
class MyTest {
57+
58+
private Calendar calendarSpy;
59+
60+
@BeforeEach
61+
void setUp() throws Exception {
62+
calendarSpy = PowerMockito.spy(Calendar.getInstance());
63+
PowerMockito.doNothing().when(calendarSpy, "clear");
64+
}
65+
}
66+
""",
67+
"""
68+
import org.junit.jupiter.api.BeforeEach;
69+
import org.powermock.api.mockito.PowerMockito;
70+
import java.util.Calendar;
71+
72+
class MyTest {
73+
74+
private Calendar calendarSpy;
75+
76+
@BeforeEach
77+
void setUp() throws Exception {
78+
calendarSpy = PowerMockito.spy(Calendar.getInstance());
79+
PowerMockito.doNothing().when(calendarSpy).clear();
80+
}
81+
}
82+
"""
83+
)
84+
);
85+
}
86+
87+
@Test
88+
void doReturnWithStringMethodName() {
89+
//language=java
90+
rewriteRun(
91+
java(
92+
"""
93+
import org.junit.jupiter.api.BeforeEach;
94+
import org.powermock.api.mockito.PowerMockito;
95+
import java.util.Calendar;
96+
97+
class MyTest {
98+
99+
private Calendar calendarSpy;
100+
101+
@BeforeEach
102+
void setUp() throws Exception {
103+
calendarSpy = PowerMockito.spy(Calendar.getInstance());
104+
PowerMockito.doReturn("gregorian").when(calendarSpy, "getCalendarType");
105+
}
106+
}
107+
""",
108+
"""
109+
import org.junit.jupiter.api.BeforeEach;
110+
import org.powermock.api.mockito.PowerMockito;
111+
import java.util.Calendar;
112+
113+
class MyTest {
114+
115+
private Calendar calendarSpy;
116+
117+
@BeforeEach
118+
void setUp() throws Exception {
119+
calendarSpy = PowerMockito.spy(Calendar.getInstance());
120+
PowerMockito.doReturn("gregorian").when(calendarSpy).getCalendarType();
121+
}
122+
}
123+
"""
124+
)
125+
);
126+
}
127+
128+
@Test
129+
void noChangeForStaticMethodStubbing() {
130+
//language=java
131+
rewriteRun(
132+
java(
133+
"""
134+
import org.junit.jupiter.api.Test;
135+
import org.powermock.api.mockito.PowerMockito;
136+
import java.util.Calendar;
137+
138+
class MyTest {
139+
140+
@Test
141+
void test() throws Exception {
142+
PowerMockito.doReturn(Calendar.getInstance()).when(Calendar.class, "getInstance");
143+
}
144+
}
145+
"""
146+
)
147+
);
148+
}
149+
150+
@Test
151+
void doStubbingWithStringMethodNameAndArgs() {
152+
//language=java
153+
rewriteRun(
154+
java(
155+
"""
156+
import org.junit.jupiter.api.BeforeEach;
157+
import org.powermock.api.mockito.PowerMockito;
158+
import java.util.Calendar;
159+
160+
class MyTest {
161+
162+
private Calendar calendarSpy;
163+
164+
@BeforeEach
165+
void setUp() throws Exception {
166+
calendarSpy = PowerMockito.spy(Calendar.getInstance());
167+
PowerMockito.doNothing().when(calendarSpy, "set", 2026, 0, 1);
168+
}
169+
}
170+
""",
171+
"""
172+
import org.junit.jupiter.api.BeforeEach;
173+
import org.powermock.api.mockito.PowerMockito;
174+
import java.util.Calendar;
175+
176+
class MyTest {
177+
178+
private Calendar calendarSpy;
179+
180+
@BeforeEach
181+
void setUp() throws Exception {
182+
calendarSpy = PowerMockito.spy(Calendar.getInstance());
183+
PowerMockito.doNothing().when(calendarSpy).set(2026, 0, 1);
184+
}
185+
}
186+
"""
187+
)
188+
);
189+
}
190+
}

0 commit comments

Comments
 (0)