diff --git a/build.gradle.kts b/build.gradle.kts index 71a58d2a0..f0ad04331 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ recipeDependencies { parserClasspath("com.github.tomakehurst:wiremock-jre8:2.35.0") parserClasspath("org.mockito:mockito-all:1.10.19") parserClasspath("org.mockito:mockito-core:3.+") + parserClasspath("org.mockito:mockito-core:5.+") parserClasspath("org.jmockit:jmockit:1.49") parserClasspath("org.jmockit:jmockit:1.22") // last version with NonStrictExpectations parserClasspath("org.mockito:mockito-junit-jupiter:3.+") diff --git a/src/main/java/org/openrewrite/java/testing/jmockit/JMockitBlockType.java b/src/main/java/org/openrewrite/java/testing/jmockit/JMockitBlockType.java index c691d97b6..7b0cbb666 100644 --- a/src/main/java/org/openrewrite/java/testing/jmockit/JMockitBlockType.java +++ b/src/main/java/org/openrewrite/java/testing/jmockit/JMockitBlockType.java @@ -22,6 +22,7 @@ @Getter enum JMockitBlockType { + MockUp, Expectations, NonStrictExpectations, Verifications, diff --git a/src/main/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockito.java b/src/main/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockito.java new file mode 100644 index 000000000..c96917adf --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockito.java @@ -0,0 +1,545 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.jmockit; + +import lombok.SneakyThrows; +import org.openrewrite.java.search.UsesType; +import static org.openrewrite.java.testing.jmockit.JMockitBlockType.MockUp; +import static org.openrewrite.java.testing.mockito.MockitoUtils.maybeAddMethodWithAnnotation; +import static org.openrewrite.java.tree.Flag.Private; +import static org.openrewrite.java.tree.Flag.Static; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.staticanalysis.LambdaBlockToExpression; +import org.openrewrite.staticanalysis.RemoveUnusedLocalVariables; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static org.openrewrite.java.testing.jmockit.JMockitBlockType.MockUp; +import static org.openrewrite.java.testing.mockito.MockitoUtils.maybeAddMethodWithAnnotation; +import static org.openrewrite.java.tree.Flag.Private; +import static org.openrewrite.java.tree.Flag.Static; + +public class JMockitMockUpToMockito extends Recipe { + private static final String MOCKITO_CLASSPATH = "mockito-core-5"; + private static final String MOCKITO_ALL_IMPORT = "org.mockito.Mockito.*"; + private static final String MOCKITO_MATCHER_IMPORT = "org.mockito.ArgumentMatchers.*"; + private static final String MOCKITO_DELEGATEANSWER_IMPORT = "org.mockito.AdditionalAnswers.delegatesTo"; + private static final String JMOCKIT_MOCKUP_IMPORT = MockUp.getFqn(); + private static final String JMOCKIT_MOCK_IMPORT = "mockit.Mock"; + private static final String MOCKITO_STATIC_PREFIX = "mockStatic"; + private static final String MOCKITO_STATIC_IMPORT = "org.mockito.MockedStatic"; + private static final String MOCKITO_MOCK_PREFIX = "mock"; + private static final String MOCKITO_CONSTRUCTION_PREFIX = "mockCons"; + private static final String MOCKITO_CONSTRUCTION_IMPORT = "org.mockito.MockedConstruction"; + + private static final String TEARDOWN_METHOD_ANNOTATION_SIGNATURE = "@org.junit.After"; + private static final String TEARDOWN_METHOD_ANNOTATION_TO_ADD = "@After"; + private static final String TEARDOWN_CLASSPATH_RESOURCE = "junit-4.13"; + private static final String TEARDOWN_IMPORT_TO_ADD = "org.junit.After"; + + @Override + public String getDisplayName() { + return "Rewrite JMockit MockUp to Mockito statements"; + } + + @Override + public String getDescription() { + return "Rewrites JMockit `MockUp` blocks to Mockito statements. This recipe will not rewrite private methods in MockUp."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>(MockUp.getFqn(), false), new JMockitMockUpToMockitoVisitor()); + } + + private static class JMockitMockUpToMockitoVisitor extends JavaIsoVisitor { + private final Map tearDownMocks = new HashMap<>(); + + /** + * Handle at class level because need to handle the case where when there is a MockUp in a setup method, and we + * need to close the migrated mockCons in the teardown, yet the teardown method comes before the setup method + */ + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + // Handle @Before/@BeforeEach mockUp + Set mds = TreeVisitor.collect( + new JavaIsoVisitor() { + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration md, ExecutionContext ctx) { + if (isSetUpMethod(md)) { + return SearchResult.found(md); + } + return super.visitMethodDeclaration(md, ctx); + } + }, + classDecl, + new HashSet<>() + ) + .stream() + .filter(J.MethodDeclaration.class::isInstance) + .map(J.MethodDeclaration.class::cast) + .collect(Collectors.toSet()); + if (mds.isEmpty()) { + return super.visitClassDeclaration(classDecl, ctx); + } + + final J.ClassDeclaration[] cd = {classDecl}; + mds.forEach(md -> md.getBody() + .getStatements() + .stream() + .filter(this::isMockUpStatement) + .map(J.NewClass.class::cast) + .forEach(newClass -> { + String className = ((J.ParameterizedType) newClass.getClazz()).getTypeParameters().get(0).toString(); + + Map mockedMethods = getMockUpMethods(newClass); + + // Add mockStatic field + if (mockedMethods.values().stream().anyMatch(m -> m.getFlags().contains(Static))) { + cd[0] = JavaTemplate.builder("private MockedStatic #{};") + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, MOCKITO_CLASSPATH)) + .imports(MOCKITO_STATIC_IMPORT) + .staticImports(MOCKITO_ALL_IMPORT) + .build() + .apply( + new Cursor(getCursor().getParentOrThrow(), cd[0]), + cd[0].getBody().getCoordinates().firstStatement(), + MOCKITO_STATIC_PREFIX + className + ); + J.VariableDeclarations mockField = (J.VariableDeclarations) cd[0].getBody().getStatements().get(0); + J.Identifier mockFieldId = mockField.getVariables().get(0).getName(); + tearDownMocks.put(MOCKITO_STATIC_PREFIX + className, mockFieldId); + } + // Add mockConstruction field + if (mockedMethods.values().stream().anyMatch(m -> !m.getFlags().contains(Static))) { + cd[0] = JavaTemplate.builder("private MockedConstruction #{};") + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, MOCKITO_CLASSPATH)) + .imports(MOCKITO_CONSTRUCTION_IMPORT) + .staticImports(MOCKITO_ALL_IMPORT) + .build() + .apply( + updateCursor(cd[0]), + cd[0].getBody().getCoordinates().firstStatement(), + MOCKITO_CONSTRUCTION_PREFIX + className + ); + J.VariableDeclarations mockField = (J.VariableDeclarations) cd[0].getBody().getStatements().get(0); + J.Identifier mockFieldId = mockField.getVariables().get(0).getName(); + tearDownMocks.put(MOCKITO_CONSTRUCTION_PREFIX + className, mockFieldId); + } + })); + + cd[0] = maybeAddMethodWithAnnotation(this, cd[0], ctx, "tearDown", + TEARDOWN_METHOD_ANNOTATION_SIGNATURE, + TEARDOWN_METHOD_ANNOTATION_TO_ADD, + TEARDOWN_CLASSPATH_RESOURCE, + TEARDOWN_IMPORT_TO_ADD, + ""); + + return super.visitClassDeclaration(cd[0], ctx); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl, ExecutionContext ctx) { + J.MethodDeclaration md = methodDecl; + if (md.getBody() == null) { + return md; + } + if (isTearDownMethod(md)) { + for (J.Identifier id : tearDownMocks.values()) { + String type = TypeUtils.asFullyQualified(id.getFieldType().getType()).getFullyQualifiedName(); + md = JavaTemplate.builder("#{any(" + type + ")}.closeOnDemand();") + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, MOCKITO_CLASSPATH)) + .imports(MOCKITO_STATIC_IMPORT, MOCKITO_CONSTRUCTION_IMPORT) + .staticImports(MOCKITO_ALL_IMPORT) + .build() + .apply( + updateCursor(md), + md.getBody().getCoordinates().lastStatement(), + id + ); + } + return md; + } + + boolean isBeforeTest = isSetUpMethod(md); + List varDeclarationInTry = new ArrayList<>(); + List mockStaticMethodInTry = new ArrayList<>(); + List mockConstructionMethodInTry = new ArrayList<>(); + List encloseStatements = new ArrayList<>(); + List residualStatements = new ArrayList<>(); + for (Statement statement : md.getBody().getStatements()) { + if (!isMockUpStatement(statement)) { + encloseStatements.add(statement); + continue; + } + + J.NewClass newClass = (J.NewClass) statement; + + // Only discard @Mock method declarations + residualStatements.addAll(newClass + .getBody() + .getStatements() + .stream() + .filter(s -> { + if (s instanceof J.MethodDeclaration) { + return ((J.MethodDeclaration) s).getLeadingAnnotations().stream() + .noneMatch(o -> TypeUtils.isOfClassType(o.getType(), JMOCKIT_MOCK_IMPORT)); + } + return true; + }) + .collect(toList()) + ); + + JavaType mockType = ((J.ParameterizedType) newClass.getClazz()).getTypeParameters().get(0).getType(); + String className = ((J.ParameterizedType) newClass.getClazz()).getTypeParameters().get(0).toString(); + + Map mockedMethods = getMockUpMethods(newClass); + + // Add MockStatic + Map mockedPublicStaticMethods = mockedMethods + .entrySet() + .stream() + .filter(m -> m.getValue().getFlags().contains(Static)) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + if (!mockedPublicStaticMethods.isEmpty()) { + if (isBeforeTest) { + String tpl = getMockStaticDeclarationInBefore(className) + + getMockStaticMethods((JavaType.Class) mockType, className, mockedPublicStaticMethods); + + md = JavaTemplate.builder(tpl) + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, MOCKITO_CLASSPATH)) + .imports(MOCKITO_STATIC_IMPORT) + .staticImports(MOCKITO_ALL_IMPORT) + .build() + .apply( + updateCursor(md), + statement.getCoordinates().after(), + tearDownMocks.get(MOCKITO_STATIC_PREFIX + className) + ); + } else { + varDeclarationInTry.add(getMockStaticDeclarationInTry(className)); + mockStaticMethodInTry.add(getMockStaticMethods((JavaType.Class) mockType, className, mockedPublicStaticMethods)); + } + + maybeAddImport(MOCKITO_STATIC_IMPORT); + } + + // Add MockConstruction + Map mockedPublicMethods = mockedMethods + .entrySet() + .stream() + .filter(m -> !m.getValue().getFlags().contains(Static)) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + if (!mockedPublicMethods.isEmpty()) { + if (isBeforeTest) { + String tpl = getMockConstructionMethods(className, mockedPublicMethods) + + getMockConstructionDeclarationInBefore(className); + + md = JavaTemplate.builder(tpl) + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, MOCKITO_CLASSPATH)) + .imports(MOCKITO_STATIC_IMPORT) + .staticImports(MOCKITO_ALL_IMPORT, MOCKITO_DELEGATEANSWER_IMPORT) + .build() + .apply( + updateCursor(md), + statement.getCoordinates().after(), + tearDownMocks.get(MOCKITO_CONSTRUCTION_PREFIX + className) + ); + } else { + varDeclarationInTry.add(getMockConstructionDeclarationInTry(className)); + mockConstructionMethodInTry.add(getMockConstructionMethods(className, mockedPublicMethods)); + } + + maybeAddImport(MOCKITO_CONSTRUCTION_IMPORT); + maybeAddImport("org.mockito.Answers", "CALLS_REAL_METHODS", false); + maybeAddImport("org.mockito.AdditionalAnswers", "delegatesTo", false); + } + + List statements = md.getBody().getStatements(); + statements.remove(statement); + md = md.withBody(md.getBody().withStatements(statements)); + } + + if (!varDeclarationInTry.isEmpty()) { + String tpl = String.join("", mockConstructionMethodInTry) + + "try (" + + String.join(";", varDeclarationInTry) + + ") {" + + String.join(";", mockStaticMethodInTry) + + "}"; + + J.MethodDeclaration residualMd = md.withBody(md.getBody().withStatements(residualStatements)); + residualMd = JavaTemplate.builder(tpl) + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, MOCKITO_CLASSPATH)) + .imports(MOCKITO_STATIC_IMPORT, MOCKITO_CONSTRUCTION_IMPORT) + .staticImports(MOCKITO_ALL_IMPORT, MOCKITO_MATCHER_IMPORT, MOCKITO_MATCHER_IMPORT, MOCKITO_DELEGATEANSWER_IMPORT) + .build() + .apply(updateCursor(residualMd), residualMd.getBody().getCoordinates().lastStatement()); + + List mdStatements = residualMd.getBody().getStatements(); + J.Try try_ = (J.Try) mdStatements.get(mdStatements.size() - 1); + + List tryStatements = try_.getBody().getStatements(); + tryStatements.addAll(encloseStatements); + try_ = try_.withBody(try_.getBody().withStatements(tryStatements)); + + mdStatements.set(mdStatements.size() - 1, try_); + md = md.withBody(residualMd.getBody().withStatements(mdStatements)); + } + + maybeAddImport(MOCKITO_ALL_IMPORT.replace(".*", ""), "*", false); + maybeRemoveImport(JMOCKIT_MOCK_IMPORT); + maybeRemoveImport(JMOCKIT_MOCKUP_IMPORT); + + doAfterVisit(new LambdaBlockToExpression().getVisitor()); + return maybeAutoFormat(methodDecl, md, ctx); + } + + private String getMatcher(JavaType s) { + maybeAddImport(MOCKITO_MATCHER_IMPORT.replace(".*", ""), "*", false); + if (s instanceof JavaType.Primitive) { + switch (s.toString()) { + case "int": + return "anyInt()"; + case "long": + return "anyLong()"; + case "double": + return "anyDouble()"; + case "float": + return "anyFloat()"; + case "short": + return "anyShort()"; + case "byte": + return "anyByte()"; + case "char": + return "anyChar()"; + case "boolean": + return "anyBoolean()"; + } + } else if (s instanceof JavaType.Array) { + String elem = TypeUtils.asArray(s).getElemType().toString(); + return "nullable(" + elem + "[].class)"; + } + return "nullable(" + TypeUtils.asFullyQualified(s).getClassName() + ".class)"; + } + + @SneakyThrows + private String getAnswerBody(J.MethodDeclaration md) { + Method findRefs = RemoveUnusedLocalVariables.class + .getDeclaredClasses()[0] + .getDeclaredMethod("findRhsReferences", J.class, J.Identifier.class); + findRefs.setAccessible(true); + + Set usedVariables = new HashSet<>(); + (new JavaIsoVisitor>() { + @Override + @SneakyThrows + public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, Set ctx) { + Cursor scope = getCursor().dropParentUntil((is) -> is instanceof J.ClassDeclaration || is instanceof J.Block || is instanceof J.MethodDeclaration || is instanceof J.ForLoop || is instanceof J.ForEachLoop || is instanceof J.ForLoop.Control || is instanceof J.ForEachLoop.Control || is instanceof J.Case || is instanceof J.Try || is instanceof J.Try.Resource || is instanceof J.Try.Catch || is instanceof J.MultiCatch || is instanceof J.Lambda || is instanceof JavaSourceFile); + List refs = (List) findRefs.invoke(null, scope.getValue(), variable.getName()); + if (!refs.isEmpty()) { + ctx.add(variable.getSimpleName()); + } + return super.visitVariable(variable, ctx); + } + }).visit(md, usedVariables); + + StringBuilder sb = new StringBuilder(); + List parameters = md.getParameters(); + for (int i = 0; i < parameters.size(); i++) { + if (!(parameters.get(i) instanceof J.VariableDeclarations)) { + continue; + } + J.VariableDeclarations vd = (J.VariableDeclarations) parameters.get(i); + String className; + if (vd.getType() instanceof JavaType.Primitive) { + className = vd.getType().toString(); + } else { + className = vd.getTypeAsFullyQualified().getClassName(); + } + String varName = vd.getVariables().get(0).getName().getSimpleName(); + if (usedVariables.contains(varName)) { + sb.append(className).append(" ").append(varName) + .append(" = invocation.getArgument(").append(i).append(");"); + } + } + + boolean hasReturn = false; + for (Statement s : md.getBody().getStatements()) { + hasReturn = hasReturn || s instanceof J.Return; + sb.append(s.print(getCursor())).append(";"); + } + // Avoid syntax error + if (!hasReturn) { + sb.append("return null;"); + } + return sb.toString(); + } + + private String getCallRealMethod(JavaType.Method m) { + return "(" + + m.getParameterTypes() + .stream() + .map(this::getMatcher) + .collect(Collectors.joining(", ")) + + ")).thenCallRealMethod();"; + } + + private String getMockStaticDeclarationInBefore(String className) { + return "#{any(" + MOCKITO_STATIC_IMPORT + ")}" + + " = mockStatic(" + className + ".class);"; + } + + private String getMockStaticDeclarationInTry(String className) { + return "MockedStatic " + MOCKITO_STATIC_PREFIX + className + + " = mockStatic(" + className + ".class)"; + } + + private String getMockStaticMethods(JavaType.Class clazz, String className, Map mockedMethods) { + StringBuilder tpl = new StringBuilder(); + + // To generate predictable method order + List keys = mockedMethods.keySet().stream() + .sorted(Comparator.comparing(o -> o.print(getCursor()))) + .collect(toList()); + for (J.MethodDeclaration m : keys) { + tpl.append("mockStatic").append(className) + .append(".when(() -> ").append(className).append(".").append(m.getSimpleName()).append("(") + .append(m.getParameters() + .stream() + .filter(J.VariableDeclarations.class::isInstance) + .map(J.VariableDeclarations.class::cast) + .map(J.VariableDeclarations::getType) + .map(this::getMatcher) + .collect(Collectors.joining(", ")) + ) + .append(")).thenAnswer(invocation -> {") + .append(getAnswerBody(m)) + .append("});"); + } + + // Call real method for non private, static methods + clazz.getMethods() + .stream() + .filter(m -> !m.isConstructor()) + .filter(m -> !m.getFlags().contains(Private)) + .filter(m -> m.getFlags().contains(Static)) + .filter(m -> !mockedMethods.containsValue(m)) + .forEach(m -> tpl.append("mockStatic").append(className).append(".when(() -> ") + .append(className).append(".").append(m.getName()) + .append(getCallRealMethod(m)) + .append(");") + ); + + return tpl.toString(); + } + + private String getMockConstructionDeclarationInBefore(String className) { + return "#{any(" + MOCKITO_CONSTRUCTION_IMPORT + ")}" + + " = mockConstructionWithAnswer(" + className + ".class, delegatesTo(" + MOCKITO_MOCK_PREFIX + className + "));"; + } + + private String getMockConstructionDeclarationInTry(String className) { + return "MockedConstruction " + MOCKITO_CONSTRUCTION_PREFIX + className + + " = mockConstructionWithAnswer(" + className + ".class, delegatesTo(" + MOCKITO_MOCK_PREFIX + className + "))"; + } + + private String getMockConstructionMethods(String className, Map mockedMethods) { + StringBuilder tpl = new StringBuilder() + .append(className) + .append(" ") + .append(MOCKITO_MOCK_PREFIX).append(className) + .append(" = mock(").append(className).append(".class, CALLS_REAL_METHODS);"); + + mockedMethods + .keySet() + .stream() + .sorted(Comparator.comparing(o -> o.print(getCursor()))) + .forEach(m -> tpl.append("doAnswer(invocation -> {") + .append(getAnswerBody(m)) + .append("}).when(").append(MOCKITO_MOCK_PREFIX).append(className).append(").").append(m.getSimpleName()).append("(") + .append(m.getParameters() + .stream() + .filter(J.VariableDeclarations.class::isInstance) + .map(J.VariableDeclarations.class::cast) + .map(J.VariableDeclarations::getType) + .map(this::getMatcher) + .collect(Collectors.joining(", ")) + ) + .append(");")); + + return tpl.toString(); + } + + private boolean isMockUpStatement(Tree tree) { + return tree instanceof J.NewClass && + ((J.NewClass) tree).getClazz() != null && + TypeUtils.isOfClassType(((J.NewClass) tree).getClazz().getType(), JMOCKIT_MOCKUP_IMPORT); + } + + private boolean isSetUpMethod(J.MethodDeclaration md) { + return md + .getLeadingAnnotations() + .stream() + .anyMatch(o -> TypeUtils.isOfClassType(o.getType(), "org.junit.Before")); + } + + private boolean isTearDownMethod(J.MethodDeclaration md) { + return md + .getLeadingAnnotations() + .stream() + .anyMatch(o -> TypeUtils.isOfClassType(o.getType(), "org.junit.After")); + } + + private Map getMockUpMethods(J.NewClass newClass) { + JavaType mockType = ((J.ParameterizedType) newClass.getClazz()).getTypeParameters().get(0).getType(); + return newClass.getBody() + .getStatements() + .stream() + .filter(J.MethodDeclaration.class::isInstance) + .map(J.MethodDeclaration.class::cast) + .filter(s -> s.getLeadingAnnotations().stream() + .anyMatch(o -> TypeUtils.isOfClassType(o.getType(), JMOCKIT_MOCK_IMPORT))) + .map(method -> { + Optional found = TypeUtils.findDeclaredMethod( + TypeUtils.asFullyQualified(mockType), + method.getSimpleName(), + method.getMethodType().getParameterTypes() + ); + if (found.isPresent()) { + JavaType.Method m = found.get(); + if (!m.getFlags().contains(Private)) { + return new AbstractMap.SimpleEntry<>(method, found.get()); + } + } + return null; + }) + .filter(Objects::nonNull) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/MockitoUtils.java b/src/main/java/org/openrewrite/java/testing/mockito/MockitoUtils.java new file mode 100644 index 000000000..52b22fdfa --- /dev/null +++ b/src/main/java/org/openrewrite/java/testing/mockito/MockitoUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.mockito; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; + +import java.util.List; +import java.util.stream.Collectors; + +public class MockitoUtils { + public static J.ClassDeclaration maybeAddMethodWithAnnotation( + JavaVisitor visitor, + J.ClassDeclaration classDecl, + ExecutionContext ctx, + String methodName, + String methodAnnotationSignature, + String methodAnnotationToAdd, + String additionalClasspathResource, + String importToAdd, + String methodAnnotationParameters + ) { + if (hasMethodWithAnnotation(classDecl, new AnnotationMatcher(methodAnnotationSignature))) { + return classDecl; + } + + J.MethodDeclaration firstTestMethod = getFirstTestMethod( + classDecl.getBody().getStatements().stream().filter(J.MethodDeclaration.class::isInstance) + .map(J.MethodDeclaration.class::cast).collect(Collectors.toList())); + + visitor.maybeAddImport(importToAdd); + return JavaTemplate.builder(methodAnnotationToAdd + methodAnnotationParameters + " public void " + methodName + "() {}") + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, additionalClasspathResource)) + .imports(importToAdd) + .build() + .apply( + new Cursor(visitor.getCursor().getParentOrThrow(), classDecl), + (firstTestMethod != null) ? + firstTestMethod.getCoordinates().before() : + classDecl.getBody().getCoordinates().lastStatement() + ); + } + + private static boolean hasMethodWithAnnotation(J.ClassDeclaration classDecl, AnnotationMatcher annotationMatcher) { + for (Statement statement : classDecl.getBody().getStatements()) { + if (statement instanceof J.MethodDeclaration) { + J.MethodDeclaration methodDeclaration = (J.MethodDeclaration) statement; + List allAnnotations = methodDeclaration.getAllAnnotations(); + for (J.Annotation annotation : allAnnotations) { + if (annotationMatcher.matches(annotation)) { + return true; + } + } + } + } + return false; + } + + private static J.@Nullable MethodDeclaration getFirstTestMethod(List methods) { + for (J.MethodDeclaration methodDeclaration : methods) { + for (J.Annotation annotation : methodDeclaration.getLeadingAnnotations()) { + if ("Test".equals(annotation.getSimpleName())) { + return methodDeclaration; + } + } + } + return null; + } +} diff --git a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockitoMockStaticToMockito.java b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockitoMockStaticToMockito.java index 019425f50..4305eabc1 100644 --- a/src/main/java/org/openrewrite/java/testing/mockito/PowerMockitoMockStaticToMockito.java +++ b/src/main/java/org/openrewrite/java/testing/mockito/PowerMockitoMockStaticToMockito.java @@ -26,6 +26,8 @@ import java.util.*; import java.util.stream.Collectors; +import static org.openrewrite.java.testing.mockito.MockitoUtils.maybeAddMethodWithAnnotation; + public class PowerMockitoMockStaticToMockito extends Recipe { @Override @@ -36,17 +38,17 @@ public String getDisplayName() { @Override public String getDescription() { return "Replaces `PowerMockito.mockStatic()` by `Mockito.mockStatic()`. Removes " + - "the `@PrepareForTest` annotation."; + "the `@PrepareForTest` annotation."; } @Override public TreeVisitor getVisitor() { return Preconditions.check( - Preconditions.or( - new UsesType<>("org.powermock..*", false), - new UsesType<>("org.mockito..*", false) - ), - new PowerMockitoToMockitoVisitor() + Preconditions.or( + new UsesType<>("org.powermock..*", false), + new UsesType<>("org.mockito..*", false) + ), + new PowerMockitoToMockitoVisitor() ); } @@ -61,9 +63,9 @@ private static class PowerMockitoToMockitoVisitor extends JavaVisitor methods) { - for (J.MethodDeclaration methodDeclaration : methods) { - for (J.Annotation annotation : methodDeclaration.getLeadingAnnotations()) { - if ("Test".equals(annotation.getSimpleName())) { - return methodDeclaration; - } - } - } - return null; - } - - private static boolean hasMethodWithAnnotation(J.ClassDeclaration classDecl, AnnotationMatcher annotationMatcher) { - for (Statement statement : classDecl.getBody().getStatements()) { - if (statement instanceof J.MethodDeclaration) { - J.MethodDeclaration methodDeclaration = (J.MethodDeclaration) statement; - List allAnnotations = methodDeclaration.getAllAnnotations(); - for (J.Annotation annotation : allAnnotations) { - if (annotationMatcher.matches(annotation)) { - return true; - } - } - } - } - return false; - } - private static boolean isStaticMockAlreadyClosed(J.Identifier staticMock, J.Block methodBody) { for (Statement statement : methodBody.getStatements()) { if (statement instanceof J.MethodInvocation) { @@ -246,7 +222,7 @@ private static boolean isStaticMockAlreadyClosed(J.Identifier staticMock, J.Bloc if (MOCKED_STATIC_CLOSE_MATCHER.matches(methodInvocation)) { if (methodInvocation.getSelect() instanceof J.Identifier) { if (((J.Identifier) methodInvocation.getSelect()).getSimpleName() - .equals(staticMock.getSimpleName())) { + .equals(staticMock.getSimpleName())) { return true; } } @@ -263,7 +239,7 @@ private static boolean isStaticMockAlreadyOpened(J.Identifier staticMock, J.Bloc if (MOCKED_STATIC_MATCHER.matches(methodInvocation)) { if (methodInvocation.getSelect() instanceof J.Identifier) { if (((J.Identifier) methodInvocation.getSelect()).getSimpleName() - .equals(staticMock.getSimpleName())) { + .equals(staticMock.getSimpleName())) { return true; } } @@ -288,15 +264,15 @@ private J.MethodDeclaration moveMockStaticMethodToSetUp(J.MethodDeclaration m, E J.MethodInvocation methodInvocation = mockStaticInvocations.get(className); if (methodInvocation != null) { m = JavaTemplate.builder("mocked#{any(org.mockito.MockedStatic)} = #{any(org.mockito.Mockito)};") - .contextSensitive() - .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12")) - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), m), - methodBody.getCoordinates().firstStatement(), - mockedTypesFieldEntry.getKey(), - methodInvocation - ); + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12")) + .build() + .apply( + new Cursor(getCursor().getParentOrThrow(), m), + methodBody.getCoordinates().firstStatement(), + mockedTypesFieldEntry.getKey(), + methodInvocation + ); } } } @@ -311,14 +287,14 @@ private J.MethodDeclaration addCloseStaticMocksOnDemandStatement(J.MethodDeclara continue; } m = JavaTemplate.builder("#{any(org.mockito.MockedStatic)}.closeOnDemand();") - .contextSensitive() - .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12")) - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), m), - methodBody.getCoordinates().lastStatement(), - mockedTypesField.getKey() - ); + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12")) + .build() + .apply( + new Cursor(getCursor().getParentOrThrow(), m), + methodBody.getCoordinates().lastStatement(), + mockedTypesField.getKey() + ); } return m; } @@ -328,14 +304,14 @@ private void determineTestGroups() { J.MethodDeclaration methodDeclarationCursor = getCursor().firstEnclosing(J.MethodDeclaration.class); if (methodDeclarationCursor != null) { Optional testAnnotation = methodDeclarationCursor - .getLeadingAnnotations().stream() - .filter(annotation -> annotation.getSimpleName().equals("Test")).findFirst(); + .getLeadingAnnotations().stream() + .filter(annotation -> annotation.getSimpleName().equals("Test")).findFirst(); testAnnotation.ifPresent( - ta -> { - if (ta.getArguments() != null) { - getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, TEST_GROUP, ta.getArguments()); - } - }); + ta -> { + if (ta.getArguments() != null) { + getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, TEST_GROUP, ta.getArguments()); + } + }); } } } @@ -354,15 +330,15 @@ private J.MethodInvocation modifyDynamicWhenMethodInvocation(J.MethodInvocation arguments.remove(0); String stringOfArguments = arguments.stream().map(Object::toString).collect(Collectors.joining(",")); method = JavaTemplate.builder("() -> #{}.#{}(#{})") - .contextSensitive() - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), method), - method.getCoordinates().replaceArguments(), - declaringClassName, - Objects.requireNonNull(calledMethod.getValue()).toString(), - stringOfArguments - ); + .contextSensitive() + .build() + .apply( + new Cursor(getCursor().getParentOrThrow(), method), + method.getCoordinates().replaceArguments(), + declaringClassName, + Objects.requireNonNull(calledMethod.getValue()).toString(), + stringOfArguments + ); method = method.withSelect(mockedField); } return method; @@ -439,17 +415,17 @@ private J.ClassDeclaration addFieldDeclarationForMockedTypes(J.ClassDeclaration continue; } classDecl = JavaTemplate.builder("private MockedStatic<#{}> " + MOCK_PREFIX + "#{};") - .contextSensitive() - .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12")) - .staticImports("org.mockito.Mockito.mockStatic") - .imports(MOCKED_STATIC) - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), classDecl), - classDecl.getBody().getCoordinates().firstStatement(), - classlessTypeName, - classlessTypeName.replace(".", "_") - ); + .contextSensitive() + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12")) + .staticImports("org.mockito.Mockito.mockStatic") + .imports(MOCKED_STATIC) + .build() + .apply( + new Cursor(getCursor().getParentOrThrow(), classDecl), + classDecl.getBody().getCoordinates().firstStatement(), + classlessTypeName, + classlessTypeName.replace(".", "_") + ); J.VariableDeclarations mockField = (J.VariableDeclarations) classDecl.getBody().getStatements().get(0); mockedTypesIdentifiers.put(mockField.getVariables().get(0).getName(), mockedStaticClass); @@ -457,7 +433,7 @@ private J.ClassDeclaration addFieldDeclarationForMockedTypes(J.ClassDeclaration getCursor().putMessage(MOCKED_TYPES_FIELDS, mockedTypesIdentifiers); maybeAutoFormat(classDecl, classDecl.withPrefix(classDecl.getPrefix(). - withWhitespace("")), classDecl.getName(), ctx, getCursor()); + withWhitespace("")), classDecl.getName(), ctx, getCursor()); maybeAddImport(MOCKED_STATIC); maybeAddImport("org.mockito.Mockito", "mockStatic"); return classDecl; @@ -465,9 +441,9 @@ private J.ClassDeclaration addFieldDeclarationForMockedTypes(J.ClassDeclaration private J.ClassDeclaration maybeAddSetUpMethodBody(J.ClassDeclaration classDecl, ExecutionContext ctx) { String testGroupsAsString = getTestGroupsAsString(); - return maybeAddMethodWithAnnotation(classDecl, ctx, "setUpStaticMocks", - setUpMethodAnnotationSignature, setUpMethodAnnotation, - additionalClasspathResource, setUpImportToAdd, testGroupsAsString); + return maybeAddMethodWithAnnotation(this, classDecl, ctx, "setUpStaticMocks", + setUpMethodAnnotationSignature, setUpMethodAnnotation, + additionalClasspathResource, setUpImportToAdd, testGroupsAsString); } private String getTestGroupsAsString() { @@ -475,55 +451,29 @@ private String getTestGroupsAsString() { String testGroupsAsString = ""; if (testGroups != null) { testGroupsAsString = "(" + - testGroups.stream().map(Object::toString).collect(Collectors.joining(",")) + - ")"; + testGroups.stream().map(Object::toString).collect(Collectors.joining(",")) + + ")"; } return testGroupsAsString; } private J.ClassDeclaration maybeAddTearDownMethodBody(J.ClassDeclaration classDecl, ExecutionContext ctx) { String testGroupsAsString = (getTestGroupsAsString().isEmpty()) ? tearDownMethodAnnotationParameters : getTestGroupsAsString(); - return maybeAddMethodWithAnnotation(classDecl, ctx, "tearDownStaticMocks", - tearDownMethodAnnotationSignature, - tearDownMethodAnnotation, - additionalClasspathResource, tearDownImportToAdd, testGroupsAsString); + return maybeAddMethodWithAnnotation(this, classDecl, ctx, "tearDownStaticMocks", + tearDownMethodAnnotationSignature, + tearDownMethodAnnotation, + additionalClasspathResource, tearDownImportToAdd, testGroupsAsString); } - private J.ClassDeclaration maybeAddMethodWithAnnotation(J.ClassDeclaration classDecl, ExecutionContext ctx, - String methodName, String methodAnnotationSignature, - String methodAnnotationToAdd, - String additionalClasspathResource, String importToAdd, - String methodAnnotationParameters) { - if (hasMethodWithAnnotation(classDecl, new AnnotationMatcher(methodAnnotationSignature))) { - return classDecl; - } - - J.MethodDeclaration firstTestMethod = getFirstTestMethod( - classDecl.getBody().getStatements().stream().filter(J.MethodDeclaration.class::isInstance) - .map(J.MethodDeclaration.class::cast).collect(Collectors.toList())); - - maybeAddImport(importToAdd); - return JavaTemplate.builder(methodAnnotationToAdd + methodAnnotationParameters + " void " + methodName + "() {}") - .contextSensitive() - .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, additionalClasspathResource)) - .imports(importToAdd) - .build() - .apply( - new Cursor(getCursor().getParentOrThrow(), classDecl), - (firstTestMethod != null) ? - firstTestMethod.getCoordinates().before() : - classDecl.getBody().getCoordinates().lastStatement() - ); - } private J.MethodInvocation modifyWhenMethodInvocation(J.MethodInvocation whenMethod) { List methodArguments = whenMethod.getArguments(); List staticMethodInvocationsInArguments = methodArguments.stream() - .filter(J.MethodInvocation.class::isInstance).map(J.MethodInvocation.class::cast) - .filter(methodInvocation -> !MOCKITO_STATIC_METHOD_MATCHER.matches(methodInvocation)) - .filter(methodInvocation -> methodInvocation.getMethodType() != null) - .filter(methodInvocation -> methodInvocation.getMethodType().hasFlags(Flag.Static)) - .collect(Collectors.toList()); + .filter(J.MethodInvocation.class::isInstance).map(J.MethodInvocation.class::cast) + .filter(methodInvocation -> !MOCKITO_STATIC_METHOD_MATCHER.matches(methodInvocation)) + .filter(methodInvocation -> methodInvocation.getMethodType() != null) + .filter(methodInvocation -> methodInvocation.getMethodType().hasFlags(Flag.Static)) + .collect(Collectors.toList()); if (staticMethodInvocationsInArguments.size() == 1) { J.MethodInvocation staticMI = staticMethodInvocationsInArguments.get(0); Expression lambdaInvocation; @@ -535,19 +485,19 @@ private J.MethodInvocation modifyWhenMethodInvocation(J.MethodInvocation whenMet return whenMethod; } if (staticMI.getArguments().stream().map(Expression::getType) - .noneMatch(Objects::nonNull)) { + .noneMatch(Objects::nonNull)) { // If the method invocation has no arguments lambdaInvocation = JavaTemplate.builder(declaringClassName + "::" + staticMI.getSimpleName()) - .contextSensitive() - .build() - .apply(new Cursor(getCursor(), staticMI), staticMI.getCoordinates().replace()); + .contextSensitive() + .build() + .apply(new Cursor(getCursor(), staticMI), staticMI.getCoordinates().replace()); } else { JavaType.Method methodType = staticMI.getMethodType(); if (methodType != null) { lambdaInvocation = JavaTemplate.builder("() -> #{any()}") - .contextSensitive() - .build() - .apply(new Cursor(getCursor(), staticMI), staticMI.getCoordinates().replace(), staticMI); + .contextSensitive() + .build() + .apply(new Cursor(getCursor(), staticMI), staticMI.getCoordinates().replace(), staticMI); } else { // do nothing lambdaInvocation = staticMI; @@ -572,18 +522,18 @@ private J.MethodInvocation modifyWhenMethodInvocation(J.MethodInvocation whenMet private J.@Nullable Identifier getFieldIdentifier(String fieldName) { return getMockedTypesFields().keySet().stream() - .filter(identifier -> identifier.getSimpleName().equals(fieldName)).findFirst() - .orElseGet(() -> { - J.ClassDeclaration cd = getCursor().dropParentUntil(J.ClassDeclaration.class::isInstance).getValue(); - return cd.getBody().getStatements().stream() - .filter(J.VariableDeclarations.class::isInstance) - .map(variableDeclarations -> ((J.VariableDeclarations) variableDeclarations).getVariables()) - .flatMap(Collection::stream) - .filter(namedVariable -> namedVariable.getSimpleName().equals(fieldName)) - .map(J.VariableDeclarations.NamedVariable::getName) - .findFirst() - .orElse(null); - }); + .filter(identifier -> identifier.getSimpleName().equals(fieldName)).findFirst() + .orElseGet(() -> { + J.ClassDeclaration cd = getCursor().dropParentUntil(J.ClassDeclaration.class::isInstance).getValue(); + return cd.getBody().getStatements().stream() + .filter(J.VariableDeclarations.class::isInstance) + .map(variableDeclarations -> ((J.VariableDeclarations) variableDeclarations).getVariables()) + .flatMap(Collection::stream) + .filter(namedVariable -> namedVariable.getSimpleName().equals(fieldName)) + .map(J.VariableDeclarations.NamedVariable::getName) + .findFirst() + .orElse(null); + }); } } } diff --git a/src/main/resources/META-INF/rewrite/classpath/mockito-core-5.13.0.jar b/src/main/resources/META-INF/rewrite/classpath/mockito-core-5.13.0.jar new file mode 100644 index 000000000..986e38d5c Binary files /dev/null and b/src/main/resources/META-INF/rewrite/classpath/mockito-core-5.13.0.jar differ diff --git a/src/main/resources/META-INF/rewrite/jmockit.yml b/src/main/resources/META-INF/rewrite/jmockit.yml index a15136715..d1ee335d7 100644 --- a/src/main/resources/META-INF/rewrite/jmockit.yml +++ b/src/main/resources/META-INF/rewrite/jmockit.yml @@ -23,6 +23,7 @@ tags: - jmockit recipeList: - org.openrewrite.java.testing.jmockit.JMockitBlockToMockito + - org.openrewrite.java.testing.jmockit.JMockitMockUpToMockito - org.openrewrite.java.testing.jmockit.JMockitAnnotatedArgumentToMockito - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: mockit.Mocked diff --git a/src/test/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockitoTest.java b/src/test/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockitoTest.java new file mode 100644 index 000000000..474763541 --- /dev/null +++ b/src/test/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockitoTest.java @@ -0,0 +1,651 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.testing.jmockit; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; + +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.SourceSpec; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.*; + +class JMockitMockUpToMockitoTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + setParserSettings(spec, JMOCKIT_DEPENDENCY, JUNIT_4_DEPENDENCY); + } + + @DocumentExample + @Test + void mockUpStaticMethodTest() { + //language=java + rewriteRun( + java( + """ + import mockit.Mock; + import mockit.MockUp; + import static org.junit.Assert.assertEquals; + import org.junit.Test; + + public class MockUpTest { + @Test + public void test() { + new MockUp() { + + @Mock + public int staticMethod() { + return 1024; + } + + @Mock + public int staticMethod(int v) { + return 128; + } + }; + assertEquals(1024, MyClazz.staticMethod()); + assertEquals(128, MyClazz.staticMethod(0)); + } + + public static class MyClazz { + public static int staticMethod() { + return 0; + } + + public static int staticMethod(int v) { + return 1; + } + } + } + """, """ + import static org.junit.Assert.assertEquals; + import static org.mockito.ArgumentMatchers.*; + import static org.mockito.Mockito.*; + + import org.junit.Test; + import org.mockito.MockedStatic; + + public class MockUpTest { + @Test + public void test() { + try (MockedStatic mockStaticMyClazz = mockStatic(MyClazz.class)) { + mockStaticMyClazz.when(() -> MyClazz.staticMethod()).thenAnswer(invocation -> 1024); + mockStaticMyClazz.when(() -> MyClazz.staticMethod(anyInt())).thenAnswer(invocation -> 128); + assertEquals(1024, MyClazz.staticMethod()); + assertEquals(128, MyClazz.staticMethod(0)); + } + } + + public static class MyClazz { + public static int staticMethod() { + return 0; + } + + public static int staticMethod(int v) { + return 1; + } + } + } + """)); + } + + @Test + void mockUpMultipleTest() { + //language=java + rewriteRun( + spec -> spec.afterTypeValidationOptions(TypeValidation.builder().identifiers(false).build()), + java( + """ + package com.openrewrite; + public static class Foo { + public String getMsg() { + return "foo"; + } + + public String getMsg(String echo) { + return "foo" + echo; + } + } + """, + SourceSpec::skip + ), + java( + """ + package com.openrewrite; + public static class Bar { + public String getMsg() { + return "bar"; + } + + public String getMsg(String echo) { + return "bar" + echo; + } + } + """, + SourceSpec::skip + ), + java( + """ + import com.openrewrite.Foo; + import com.openrewrite.Bar; + import org.junit.Test; + import mockit.Mock; + import mockit.MockUp; + import static org.junit.Assert.assertEquals; + + public class MockUpTest { + @Test + public void test() { + new MockUp() { + @Mock + public String getMsg() { + return "FOO"; + } + @Mock + public String getMsg(String echo) { + return "FOO" + echo; + } + }; + new MockUp() { + @Mock + public String getMsg() { + return "BAR"; + } + @Mock + public String getMsg(String echo) { + return "BAR" + echo; + } + }; + assertEquals("FOO", new Foo().getMsg()); + assertEquals("FOOecho", new Foo().getMsg("echo")); + assertEquals("BAR", new Bar().getMsg()); + assertEquals("BARecho", new Bar().getMsg("echo")); + } + } + """, """ + import com.openrewrite.Foo; + import com.openrewrite.Bar; + import org.junit.Test; + import org.mockito.MockedConstruction; + import static org.junit.Assert.assertEquals; + import static org.mockito.AdditionalAnswers.delegatesTo; + import static org.mockito.Answers.CALLS_REAL_METHODS; + import static org.mockito.ArgumentMatchers.*; + import static org.mockito.Mockito.*; + + public class MockUpTest { + @Test + public void test() { + Foo mockFoo = mock(Foo.class, CALLS_REAL_METHODS); + doAnswer(invocation -> "FOO").when(mockFoo).getMsg(); + doAnswer(invocation -> { + String echo = invocation.getArgument(0); + return "FOO" + echo; + }).when(mockFoo).getMsg(nullable(String.class)); + Bar mockBar = mock(Bar.class, CALLS_REAL_METHODS); + doAnswer(invocation -> "BAR").when(mockBar).getMsg(); + doAnswer(invocation -> { + String echo = invocation.getArgument(0); + return "BAR" + echo; + }).when(mockBar).getMsg(nullable(String.class)); + try (MockedConstruction mockConsFoo = mockConstructionWithAnswer(Foo.class, delegatesTo(mockFoo));MockedConstruction mockConsBar = mockConstructionWithAnswer(Bar.class, delegatesTo(mockBar))) { + assertEquals("FOO", new Foo().getMsg()); + assertEquals("FOOecho", new Foo().getMsg("echo")); + assertEquals("BAR", new Bar().getMsg()); + assertEquals("BARecho", new Bar().getMsg("echo")); + } + } + } + """) + ); + } + + @Test + void mockUpInnerStatementTest() { + //language=java + rewriteRun( + java( + """ + import mockit.Mock; + import mockit.MockUp; + + import org.junit.Test; + import static org.junit.Assert.assertEquals; + + public class MockUpTest { + @Test + public void test() { + new MockUp() { + final String msg = "newMsg"; + + @Mock + public String getMsg() { + return msg; + } + }; + + // Should ignore the newClass statement + new Runnable() { + @Override + public void run() { + System.out.println("run"); + } + }; + assertEquals("newMsg", new MyClazz().getMsg()); + } + + public static class MyClazz { + public String getMsg() { + return "msg"; + } + } + } + """, """ + import org.junit.Test; + import org.mockito.MockedConstruction; + + import static org.junit.Assert.assertEquals; + import static org.mockito.AdditionalAnswers.delegatesTo; + import static org.mockito.Answers.CALLS_REAL_METHODS; + import static org.mockito.Mockito.*; + + public class MockUpTest { + @Test + public void test() { + final String msg = "newMsg"; + MyClazz mockMyClazz = mock(MyClazz.class, CALLS_REAL_METHODS); + doAnswer(invocation -> msg).when(mockMyClazz).getMsg(); + try (MockedConstruction mockConsMyClazz = mockConstructionWithAnswer(MyClazz.class, delegatesTo(mockMyClazz))) { + + // Should ignore the newClass statement + new Runnable() { + @Override + public void run() { + System.out.println("run"); + } + }; + assertEquals("newMsg", new MyClazz().getMsg()); + } + } + + public static class MyClazz { + public String getMsg() { + return "msg"; + } + } + } + """)); + } + + @Test + void mockUpVoidTest() { + //language=java + rewriteRun( + java( + """ + import mockit.Mock; + import mockit.MockUp; + import static org.junit.Assert.assertEquals; + import org.junit.Test; + + public class MockUpTest { + @Test + public void test() { + new MockUp() { + @Mock + public void changeMsg() { + MockUpClass.Save.msg = "mockMsg"; + } + + @Mock + public void changeText(String text) { + MockUpClass.Save.text = "mockText"; + } + }; + + assertEquals("mockMsg", new MockUpClass().getMsg()); + assertEquals("mockText", new MockUpClass().getText()); + } + + public static class MockUpClass { + public static class Save { + public static String msg = "msg"; + public static String text = "text"; + } + + public final String getMsg() { + changeMsg(); + return Save.msg; + } + + public void changeMsg() { + Save.msg = "newMsg"; + } + + public String getText() { + changeText("newText"); + return Save.text; + } + + public static void changeText(String text) { + Save.text = text; + } + } + } + """, + """ + import static org.junit.Assert.assertEquals; + import static org.mockito.AdditionalAnswers.delegatesTo; + import static org.mockito.Answers.CALLS_REAL_METHODS; + import static org.mockito.ArgumentMatchers.*; + import static org.mockito.Mockito.*; + + import org.junit.Test; + import org.mockito.MockedConstruction; + import org.mockito.MockedStatic; + + public class MockUpTest { + @Test + public void test() { + MockUpClass mockMockUpClass = mock(MockUpClass.class, CALLS_REAL_METHODS); + doAnswer(invocation -> { + MockUpClass.Save.msg = "mockMsg"; + return null; + }).when(mockMockUpClass).changeMsg(); + try (MockedStatic mockStaticMockUpClass = mockStatic(MockUpClass.class);MockedConstruction mockConsMockUpClass = mockConstructionWithAnswer(MockUpClass.class, delegatesTo(mockMockUpClass))) { + mockStaticMockUpClass.when(() -> MockUpClass.changeText(nullable(String.class))).thenAnswer(invocation -> { + String text = invocation.getArgument(0); + MockUpClass.Save.text = "mockText"; + return null; + }); + + assertEquals("mockMsg", new MockUpClass().getMsg()); + assertEquals("mockText", new MockUpClass().getText()); + } + } + + public static class MockUpClass { + public static class Save { + public static String msg = "msg"; + public static String text = "text"; + } + + public final String getMsg() { + changeMsg(); + return Save.msg; + } + + public void changeMsg() { + Save.msg = "newMsg"; + } + + public String getText() { + changeText("newText"); + return Save.text; + } + + public static void changeText(String text) { + Save.text = text; + } + } + } + """)); + } + + @Test + void mockUpAtSetUpWithoutTearDownTest() { + rewriteRun( + java( + """ + import org.junit.Before; + import org.junit.Test; + import mockit.Mock; + import mockit.MockUp; + import static org.junit.Assert.assertEquals; + + public class MockUpTest { + @Before + public void setUp() { + new MockUp() { + @Mock + public String getMsg() { + return "mockMsg"; + } + }; + } + + @Test + public void test() { + assertEquals("mockMsg", new MyClazz().getMsg()); + } + + public static class MyClazz { + public String getMsg() { + return "msg"; + } + } + } + """, + """ + import org.junit.After; + import org.junit.Before; + import org.junit.Test; + import org.mockito.MockedConstruction; + import static org.junit.Assert.assertEquals; + import static org.mockito.AdditionalAnswers.delegatesTo; + import static org.mockito.Answers.CALLS_REAL_METHODS; + import static org.mockito.Mockito.*; + + public class MockUpTest { + private MockedConstruction mockConsMyClazz; + + @Before + public void setUp() { + MyClazz mockMyClazz = mock(MyClazz.class, CALLS_REAL_METHODS); + doAnswer(invocation -> "mockMsg").when(mockMyClazz).getMsg(); + mockConsMyClazz = mockConstructionWithAnswer(MyClazz.class, delegatesTo(mockMyClazz)); + } + + @After + public void tearDown() { + mockConsMyClazz.closeOnDemand(); + } + + @Test + public void test() { + assertEquals("mockMsg", new MyClazz().getMsg()); + } + + public static class MyClazz { + public String getMsg() { + return "msg"; + } + } + } + """ + ) + ); + } + + @Test + void mockUpAtSetUpWithTearDownTest() { + rewriteRun( + java( + """ + import org.junit.Before; + import org.junit.After; + import org.junit.Test; + import mockit.Mock; + import mockit.MockUp; + import static org.junit.Assert.assertEquals; + + public class MockUpTest { + @Before + public void setUp() { + new MockUp() { + @Mock + public String getMsg() { + return "mockMsg"; + } + + @Mock + public String getStaticMsg() { + return "mockStaticMsg"; + } + }; + } + + @After + public void tearDown() { + } + + @Test + public void test() { + assertEquals("mockMsg", new MyClazz().getMsg()); + assertEquals("mockStaticMsg", MyClazz.getStaticMsg()); + } + + public static class MyClazz { + public String getMsg() { + return "msg"; + } + + public static String getStaticMsg() { + return "staticMsg"; + } + } + } + """, + """ + import org.junit.Before; + import org.junit.After; + import org.junit.Test; + import org.mockito.MockedConstruction; + import org.mockito.MockedStatic; + import static org.junit.Assert.assertEquals; + import static org.mockito.AdditionalAnswers.delegatesTo; + import static org.mockito.Answers.CALLS_REAL_METHODS; + import static org.mockito.Mockito.*; + + public class MockUpTest { + private MockedConstruction mockConsMyClazz; + private MockedStatic mockStaticMyClazz; + + @Before + public void setUp() { + MyClazz mockMyClazz = mock(MyClazz.class, CALLS_REAL_METHODS); + doAnswer(invocation -> "mockMsg").when(mockMyClazz).getMsg(); + mockConsMyClazz = mockConstructionWithAnswer(MyClazz.class, delegatesTo(mockMyClazz)); + mockStaticMyClazz = mockStatic(MyClazz.class); + mockStaticMyClazz.when(() -> MyClazz.getStaticMsg()).thenAnswer(invocation -> "mockStaticMsg"); + } + + @After + public void tearDown() { + mockConsMyClazz.closeOnDemand(); + mockStaticMyClazz.closeOnDemand(); + } + + @Test + public void test() { + assertEquals("mockMsg", new MyClazz().getMsg()); + assertEquals("mockStaticMsg", MyClazz.getStaticMsg()); + } + + public static class MyClazz { + public String getMsg() { + return "msg"; + } + + public static String getStaticMsg() { + return "staticMsg"; + } + } + } + """ + ) + ); + } + + @Test + void mockUpWithParamsTest() { + rewriteRun( + java( + """ + import mockit.Mock; + import mockit.MockUp; + import org.junit.Test; + + import static org.junit.Assert.assertEquals; + + public class MockUpTest { + @Test + public void init() { + new MockUp() { + @Mock + public String getMsg(String foo, String bar, String unused) { + return foo + bar; + } + }; + assertEquals("foobar", new MyClazz().getMsg("foo", "bar", "unused")); + } + + public static class MyClazz { + public String getMsg(String foo, String bar, String unused) { + return "msg"; + } + } + } + """, + """ + import org.junit.Test; + import org.mockito.MockedConstruction; + + import static org.junit.Assert.assertEquals; + import static org.mockito.AdditionalAnswers.delegatesTo; + import static org.mockito.Answers.CALLS_REAL_METHODS; + import static org.mockito.ArgumentMatchers.*; + import static org.mockito.Mockito.*; + + public class MockUpTest { + @Test + public void init() { + MyClazz mockMyClazz = mock(MyClazz.class, CALLS_REAL_METHODS); + doAnswer(invocation -> { + String foo = invocation.getArgument(0); + String bar = invocation.getArgument(1); + return foo + bar; + }).when(mockMyClazz).getMsg(nullable(String.class), nullable(String.class), nullable(String.class)); + try (MockedConstruction mockConsMyClazz = mockConstructionWithAnswer(MyClazz.class, delegatesTo(mockMyClazz))) { + assertEquals("foobar", new MyClazz().getMsg("foo", "bar", "unused")); + } + } + + public static class MyClazz { + public String getMsg(String foo, String bar, String unused) { + return "msg"; + } + } + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/testing/jmockit/JMockitNonStrictExpectationsToMockitoTest.java b/src/test/java/org/openrewrite/java/testing/jmockit/JMockitNonStrictExpectationsToMockitoTest.java index bc186bb48..75227e326 100644 --- a/src/test/java/org/openrewrite/java/testing/jmockit/JMockitNonStrictExpectationsToMockitoTest.java +++ b/src/test/java/org/openrewrite/java/testing/jmockit/JMockitNonStrictExpectationsToMockitoTest.java @@ -21,12 +21,9 @@ import org.openrewrite.test.RewriteTest; import static org.openrewrite.java.Assertions.java; -import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.MOCKITO_CORE_DEPENDENCY; -import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.setParserSettings; +import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.*; class JMockitNonStrictExpectationsToMockitoTest implements RewriteTest { - - private static final String JUNIT_4_DEPENDENCY = "junit-4.13.2"; private static final String LEGACY_JMOCKIT_DEPENDENCY = "jmockit-1.22"; @Override diff --git a/src/test/java/org/openrewrite/java/testing/jmockit/JMockitTestUtils.java b/src/test/java/org/openrewrite/java/testing/jmockit/JMockitTestUtils.java index c66d3fbb1..9c2ac5f31 100644 --- a/src/test/java/org/openrewrite/java/testing/jmockit/JMockitTestUtils.java +++ b/src/test/java/org/openrewrite/java/testing/jmockit/JMockitTestUtils.java @@ -24,6 +24,7 @@ public class JMockitTestUtils { static final String MOCKITO_CORE_DEPENDENCY = "mockito-core-3.12"; static final String JUNIT_5_JUPITER_DEPENDENCY = "junit-jupiter-api-5.9"; + static final String JUNIT_4_DEPENDENCY = "junit-4.13.2"; static final String JMOCKIT_DEPENDENCY = "jmockit-1.49"; static final String MOCKITO_JUPITER_DEPENDENCY = "mockito-junit-jupiter-3.12";