Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JMockit to Mockito Recipe - Handle Argument Matchers and Void Methods #419

Merged
merged 13 commits into from
Oct 30, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
*/
package org.openrewrite.java.testing.jmockit;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.regex.Pattern;

import lombok.EqualsAndHashCode;
Expand All @@ -31,57 +29,61 @@
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaCoordinates;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.java.tree.*;

@Value
@EqualsAndHashCode(callSuper = false)
public class JMockitExpectationsToMockitoWhen extends Recipe {
public class JMockitExpectationsToMockito extends Recipe {
@Override
public String getDisplayName() {
return "Rewrite JMockit Expectations";
}

@Override
public String getDescription() {
return "Rewrites JMockit `Expectations` to `Mockito.when`.";
return "Rewrites JMockit `Expectations` blocks to Mockito statements.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesType<>("mockit.*", false),
return Preconditions.check(new UsesType<>("mockit.Expectations", false),
new RewriteExpectationsVisitor());
}

private static class RewriteExpectationsVisitor extends JavaIsoVisitor<ExecutionContext> {

private static final String VOID_RESULT_TEMPLATE = "doNothing().when(#{any(java.lang.String)});";
private static final String PRIMITIVE_RESULT_TEMPLATE = "when(#{any()}).thenReturn(#{});";
private static final String OBJECT_RESULT_TEMPLATE = "when(#{any()}).thenReturn(#{any(java.lang.String)});";
private static final String EXCEPTION_RESULT_TEMPLATE = "when(#{any()}).thenThrow(#{any()});";
private static final Pattern EXPECTATIONS_PATTERN = Pattern.compile("mockit.Expectations");

// the LST element that is being updated when applying one of the java templates
private Object cursorLocation;

// the coordinates where the next statement should be inserted
private JavaCoordinates coordinates;
private static final String THROWABLE_RESULT_TEMPLATE = "when(#{any()}).thenThrow(#{any()});";
private static final Set<String> JMOCKIT_ARGUMENT_MATCHERS = new HashSet<>();
static {
JMOCKIT_ARGUMENT_MATCHERS.add("anyString");
JMOCKIT_ARGUMENT_MATCHERS.add("anyInt");
JMOCKIT_ARGUMENT_MATCHERS.add("anyLong");
JMOCKIT_ARGUMENT_MATCHERS.add("anyDouble");
JMOCKIT_ARGUMENT_MATCHERS.add("anyFloat");
JMOCKIT_ARGUMENT_MATCHERS.add("anyBoolean");
JMOCKIT_ARGUMENT_MATCHERS.add("anyByte");
JMOCKIT_ARGUMENT_MATCHERS.add("anyChar");
JMOCKIT_ARGUMENT_MATCHERS.add("anyShort");
JMOCKIT_ARGUMENT_MATCHERS.add("any");
}

@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDeclaration, ExecutionContext ctx) {
J.MethodDeclaration md = super.visitMethodDeclaration(methodDeclaration, ctx);
if (md.getBody() == null) {
return md;
}
cursorLocation = md.getBody();
// the LST element that is being updated when applying a java template
Object cursorLocation = md.getBody();
J.Block newBody = md.getBody();
List<Statement> statements = md.getBody().getStatements();

// iterate over each statement in the method body, find Expectations blocks and rewrite them
for (int i = 0; i < statements.size(); i++) {
Statement s = statements.get(i);
for (int bodyStatementIndex = 0; bodyStatementIndex < statements.size(); bodyStatementIndex++) {
Statement s = statements.get(bodyStatementIndex);
if (!(s instanceof J.NewClass)) {
continue;
}
Expand All @@ -90,7 +92,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
continue;
}
J.Identifier clazz = (J.Identifier) nc.getClazz();
if (clazz.getType() == null || !clazz.getType().isAssignableFrom(EXPECTATIONS_PATTERN)) {
if (!TypeUtils.isAssignableTo("mockit.Expectations", clazz.getType())) {
continue;
}
// empty Expectations block is considered invalid
Expand All @@ -99,28 +101,33 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
assert nc.getBody().getStatements().size() == 1 : "Expectations block is malformed";

// we have a valid Expectations block, update imports and rewrite with Mockito statements
maybeAddImport("org.mockito.Mockito", "when");
maybeRemoveImport("mockit.Expectations");

// the first coordinates are the coordinates the Expectations block, replacing it
coordinates = nc.getCoordinates().replace();
// the first coordinates are the coordinates of the Expectations block, replacing it
JavaCoordinates coordinates = nc.getCoordinates().replace();
J.Block expectationsBlock = (J.Block) nc.getBody().getStatements().get(0);
List<Statement> expectationStatements = expectationsBlock.getStatements();
List<Object> templateParams = new ArrayList<>();

// iterate over the expectations statements and rebuild the method body
for (Statement expectationStatement : expectationStatements) {
// TODO: handle void methods (including final statement)

int mockitoStatementIndex = 0;
for (Statement expectationStatement : expectationsBlock.getStatements()) {
// TODO: handle additional jmockit expectations features

if (expectationStatement instanceof J.MethodInvocation) {
if (!templateParams.isEmpty()) {
// apply template to build new method body
newBody = buildNewBody(ctx, templateParams, i);
newBody = applyTemplate(ctx, templateParams, cursorLocation, coordinates);

// next statement coordinates are immediately after the statement just added
int newStatementIndex = bodyStatementIndex + mockitoStatementIndex;
coordinates = newBody.getStatements().get(newStatementIndex).getCoordinates().after();

// cursor location is now the new body
cursorLocation = newBody;

// reset template params for next expectation
templateParams = new ArrayList<>();
mockitoStatementIndex += 1;
}
templateParams.add(expectationStatement);
} else {
Expand All @@ -131,60 +138,76 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl

// handle the last statement
if (!templateParams.isEmpty()) {
newBody = buildNewBody(ctx, templateParams, i);
newBody = applyTemplate(ctx, templateParams, cursorLocation, coordinates);
}
}

return md.withBody(newBody);
}

private J.Block buildNewBody(ExecutionContext ctx, List<Object> templateParams, int newStatementIndex) {
Expression result = (Expression) templateParams.get(1);
String template = getTemplate(result);

J.Block newBody = JavaTemplate.builder(template)
private J.Block applyTemplate(ExecutionContext ctx, List<Object> templateParams, Object cursorLocation,
JavaCoordinates coordinates) {
Expression result = null;
String methodName = "doNothing";
if (templateParams.size() > 1) {
methodName = "when";
result = (Expression) templateParams.get(1);
}
maybeAddImport("org.mockito.Mockito", methodName);
rewriteArgumentMatchers(ctx, templateParams);
return JavaTemplate.builder(getMockitoStatementTemplate(result))
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12"))
.staticImports("org.mockito.Mockito.when")
.staticImports("org.mockito.Mockito." + methodName)
.build()
.apply(
new Cursor(getCursor(), cursorLocation),
coordinates,
templateParams.toArray()
);
}

List<Statement> newStatements = new ArrayList<>(newBody.getStatements().size());
for (int i = 0; i < newBody.getStatements().size(); i++) {
Statement s = newBody.getStatements().get(i);
if (i == newStatementIndex) {
// next statement coordinates are immediately after the statement just added
coordinates = s.getCoordinates().after();
private void rewriteArgumentMatchers(ExecutionContext ctx, List<Object> templateParams) {
J.MethodInvocation invocation = (J.MethodInvocation) templateParams.get(0);
List<Expression> newArguments = new ArrayList<>(invocation.getArguments().size());
for (Expression methodArgument : invocation.getArguments()) {
if (!isArgumentMatcher(methodArgument)) {
newArguments.add(methodArgument);
continue;
}
newStatements.add(s);
String argumentMatcher = ((J.Identifier) methodArgument).getSimpleName();
maybeAddImport("org.mockito.Mockito", argumentMatcher);
newArguments.add(JavaTemplate.builder(argumentMatcher + "()")
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12"))
.staticImports("org.mockito.Mockito." + argumentMatcher)
.build()
.apply(
new Cursor(getCursor(), methodArgument),
methodArgument.getCoordinates().replace()
));
}
newBody = newBody.withStatements(newStatements);

// cursor location is now the new body
cursorLocation = newBody;
templateParams.set(0, invocation.withArguments(newArguments));
}

return newBody;
private static boolean isArgumentMatcher(Expression expression) {
if (!(expression instanceof J.Identifier)) {
return false;
}
J.Identifier identifier = (J.Identifier) expression;
return JMOCKIT_ARGUMENT_MATCHERS.contains(identifier.getSimpleName());
}

/*
* Based on the result type, we need to use a different template.
*/
private static String getTemplate(Expression result) {
private static String getMockitoStatementTemplate(Expression result) {
if (result == null) {
return VOID_RESULT_TEMPLATE;
}
String template;
JavaType resultType = Objects.requireNonNull(result.getType());
if (resultType instanceof JavaType.Primitive) {
template = PRIMITIVE_RESULT_TEMPLATE;
} else if (resultType instanceof JavaType.Class) {
Class<?> resultClass;
try {
resultClass = Class.forName(((JavaType.Class) resultType).getFullyQualifiedName());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
template = Throwable.class.isAssignableFrom(resultClass) ? EXCEPTION_RESULT_TEMPLATE : OBJECT_RESULT_TEMPLATE;
template = TypeUtils.isAssignableTo(Throwable.class.getName(), resultType)
? THROWABLE_RESULT_TEMPLATE
: OBJECT_RESULT_TEMPLATE;
} else {
throw new IllegalStateException("Unexpected value: " + result.getType());
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/META-INF/rewrite/jmockit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ recipeList:
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: mockit.integration.junit5.JMockitExtension
newFullyQualifiedTypeName: org.mockito.junit.jupiter.MockitoExtension
- org.openrewrite.java.testing.jmockit.JMockitExpectationsToMockitoWhen
- org.openrewrite.java.testing.jmockit.JMockitExpectationsToMockito
- org.openrewrite.java.dependencies.AddDependency:
groupId: org.mockito
artifactId: mockito-core
Expand Down
Loading
Loading