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 mockito full verifications and Refactor tests #617

Merged
merged 11 commits into from
Oct 22, 2024
Original file line number Diff line number Diff line change
@@ -29,13 +29,14 @@
import java.util.ArrayList;
import java.util.List;

import static org.openrewrite.java.testing.jmockit.JMockitBlockType.FullVerifications;
import static org.openrewrite.java.testing.jmockit.JMockitBlockType.NonStrictExpectations;
import static org.openrewrite.java.testing.jmockit.JMockitBlockType.Verifications;

class JMockitBlockRewriter {

private static final String WHEN_TEMPLATE_PREFIX = "when(#{any()}).";
private static final String VERIFY_TEMPLATE_PREFIX = "verify(#{any()}";
private static final String VERIFY_NO_INTERACTIONS_TEMPLATE_PREFIX = "verifyNoMoreInteractions(";
private static final String LENIENT_TEMPLATE_PREFIX = "lenient().";

private static final String RETURN_TEMPLATE_PREFIX = "thenReturn(";
@@ -95,17 +96,27 @@ J.Block rewriteMethodBody() {

// iterate over the statements and build a list of grouped method invocations and related statements eg times
List<List<Statement>> methodInvocationsToRewrite = new ArrayList<>();
List<J.Identifier> uniqueMocks = new ArrayList<>();
int methodInvocationIdx = -1;
for (Statement jmockitBlockStatement : jmockitBlock.getStatements()) {
if (jmockitBlockStatement instanceof J.MethodInvocation) {
// ensure it's not a returns statement, we add that later to related statements
J.MethodInvocation invocation = (J.MethodInvocation) jmockitBlockStatement;
if (invocation.getSelect() != null && !invocation.getName().getSimpleName().equals("returns")) {
methodInvocationIdx++;
methodInvocationsToRewrite.add(new ArrayList<>());
Expression select = invocation.getSelect();
if (select instanceof J.Identifier) {
J.Identifier mockObj = (J.Identifier) select;
// ensure it's not a returns statement, we add that later to related statements
if (!invocation.getName().getSimpleName().equals("returns")) {
methodInvocationIdx++;
methodInvocationsToRewrite.add(new ArrayList<>());
}
if (isFullVerifications() && uniqueMocks.stream().noneMatch(mock -> mock.getType().equals(mockObj.getType())
&& mock.getSimpleName().equals(mockObj.getSimpleName()))) {
uniqueMocks.add(mockObj);
}
}
}

// add the statements corresponding to the method invocation
if (methodInvocationIdx != -1) {
methodInvocationsToRewrite.get(methodInvocationIdx).add(jmockitBlockStatement);
}
@@ -118,9 +129,17 @@ J.Block rewriteMethodBody() {

// now rewrite
methodInvocationsToRewrite.forEach(this::rewriteMethodInvocation);

if (isFullVerifications()) {
rewriteFullVerify(new ArrayList<>(uniqueMocks));
}
return methodBody;
}

private boolean isFullVerifications() {
return this.blockType == FullVerifications;
}

private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
final MockInvocationResults mockInvocationResults = buildMockInvocationResults(statementsToRewrite);
if (mockInvocationResults == null) {
@@ -136,7 +155,7 @@ private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
rewriteResult(invocation, mockInvocationResults.getResults(), hasTimes);
}

if (!hasResults && !hasTimes && (this.blockType == JMockitBlockType.Expectations || this.blockType == Verifications)) {
if (!hasResults && !hasTimes && (this.blockType == JMockitBlockType.Expectations || this.blockType.isVerifications())) {
rewriteVerify(invocation, null, "");
return;
}
@@ -171,7 +190,7 @@ private void rewriteResult(J.MethodInvocation invocation, List<Expression> resul
List<Object> templateParams = new ArrayList<>();
templateParams.add(invocation);
templateParams.addAll(results);
this.rewriteFailed = !rewriteTemplate(template, templateParams, nextStatementCoordinates);
rewriteTemplate(template, templateParams, nextStatementCoordinates);
if (this.rewriteFailed) {
return;
}
@@ -199,19 +218,19 @@ private void rewriteVerify(J.MethodInvocation invocation, @Nullable Expression t
templateParams.add(invocation.getName().getSimpleName());
String verifyTemplate = getVerifyTemplate(invocation.getArguments(), verificationMode, templateParams);
JavaCoordinates verifyCoordinates;
if (this.blockType == Verifications) {
if (this.blockType.isVerifications()) {
// for Verifications, replace the Verifications block
verifyCoordinates = nextStatementCoordinates;
} else {
// for Expectations put the verify at the end of the method
verifyCoordinates = methodBody.getCoordinates().lastStatement();
}
this.rewriteFailed = !rewriteTemplate(verifyTemplate, templateParams, verifyCoordinates);
rewriteTemplate(verifyTemplate, templateParams, verifyCoordinates);
if (this.rewriteFailed) {
return;
}

if (this.blockType == Verifications) {
if (this.blockType.isVerifications()) {
setNextStatementCoordinates(++numStatementsAdded); // for Expectations, verify statements added to end of method
}

@@ -223,6 +242,20 @@ private void rewriteVerify(J.MethodInvocation invocation, @Nullable Expression t
}
}

private void rewriteFullVerify(List<Object> mocks) {
if (!mocks.isEmpty()) {
StringBuilder sb = new StringBuilder(VERIFY_NO_INTERACTIONS_TEMPLATE_PREFIX);
mocks.forEach(mock -> sb.append(ANY_TEMPLATE_FIELD).append(",")); // verifyNoMoreInteractions(mock1, mock2 ...
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
rewriteTemplate(sb.toString(), mocks, nextStatementCoordinates);
if (!this.rewriteFailed) {
setNextStatementCoordinates(++numStatementsAdded);
visitor.maybeAddImport(MOCKITO_IMPORT_FQN_PREFX, "verifyNoMoreInteractions", false);
}
}
}

private void setNextStatementCoordinates(int numStatementsAdded) {
if (numStatementsAdded <= 0 && bodyStatementIndex == 0) {
nextStatementCoordinates = methodBody.getCoordinates().firstStatement();
@@ -240,7 +273,7 @@ private void setNextStatementCoordinates(int numStatementsAdded) {
this.nextStatementCoordinates = this.methodBody.getStatements().get(lastStatementIdx).getCoordinates().after();
}

private boolean rewriteTemplate(String template, List<Object> templateParams, JavaCoordinates
private void rewriteTemplate(String template, List<Object> templateParams, JavaCoordinates
rewriteCoords) {
int numStatementsBefore = methodBody.getStatements().size();
methodBody = JavaTemplate.builder(template)
@@ -252,7 +285,7 @@ private boolean rewriteTemplate(String template, List<Object> templateParams, Ja
rewriteCoords,
templateParams.toArray()
);
return methodBody.getStatements().size() > numStatementsBefore;
this.rewriteFailed = methodBody.getStatements().size() <= numStatementsBefore;
}

private @Nullable String getWhenTemplate(List<Expression> results, boolean lenient) {
Original file line number Diff line number Diff line change
@@ -26,31 +26,34 @@
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Statement;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.openrewrite.java.testing.jmockit.JMockitBlockType.*;
import static org.openrewrite.java.testing.jmockit.JMockitBlockType.getSupportedTypesStr;
import static org.openrewrite.java.testing.jmockit.JMockitBlockType.values;

@Value
@EqualsAndHashCode(callSuper = false)
public class JMockitBlockToMockito extends Recipe {

private static final String SUPPORTED_TYPES = getSupportedTypesStr();

@Override
public String getDisplayName() {
return "Rewrite JMockit Expectations, Verifications and NonStrictExpectations";
return "Rewrite JMockit " + SUPPORTED_TYPES;
}

@Override
public String getDescription() {
return "Rewrites JMockit `Expectations, Verifications and NonStrictExpectations` blocks to Mockito statements.";
return "Rewrites JMockit `" + SUPPORTED_TYPES + "` blocks to Mockito statements.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(Preconditions.or(
new UsesType<>(Expectations.getFqn(), false),
new UsesType<>(Verifications.getFqn(), false),
new UsesType<>(NonStrictExpectations.getFqn(), false)), new RewriteJMockitBlockVisitor());
@SuppressWarnings("rawtypes")
UsesType[] usesTypes = Arrays.stream(values()).map(blockType -> new UsesType<>(blockType.getFqn(), false)).toArray(UsesType[]::new);
return Preconditions.check(Preconditions.or(usesTypes), new RewriteJMockitBlockVisitor());
}

private static class RewriteJMockitBlockVisitor extends JavaIsoVisitor<ExecutionContext> {
Original file line number Diff line number Diff line change
@@ -17,16 +17,25 @@

import lombok.Getter;

import java.util.Arrays;

@Getter
enum JMockitBlockType {

Expectations,
NonStrictExpectations,
Verifications,
NonStrictExpectations;
FullVerifications;

private final String fqn = "mockit." + this.name();

private final String fqn;
boolean isVerifications() {
return this == Verifications || this == FullVerifications;
}

JMockitBlockType() {
this.fqn = "mockit." + this.name();
static String getSupportedTypesStr() {
StringBuilder sb = new StringBuilder();
Arrays.stream(values()).forEach(value -> sb.append(value).append(", "));
return sb.substring(0, sb.length() - 2);
}
}
Original file line number Diff line number Diff line change
@@ -17,28 +17,10 @@

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

class JMockitAnnotatedArgumentToMockitoTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec
.parser(JavaParser.fromJavaVersion()
.logCompilationWarningsAndErrors(true)
.classpathFromResources(new InMemoryExecutionContext(),
"junit-jupiter-api-5.9",
"jmockit-1.49"
))
.recipeFromResource(
"/META-INF/rewrite/jmockit.yml",
"org.openrewrite.java.testing.jmockit.JMockitToMockito"
);
}
class JMockitAnnotatedArgumentToMockitoTest extends JMockitTestBase {

@DocumentExample
@Test
Original file line number Diff line number Diff line change
@@ -19,11 +19,8 @@
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.Issue;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;
import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.setDefaultParserSettings;

/**
* At the moment, JMockit Delegates are not migrated to mockito. What I'm seeing is that they are being trashed
@@ -32,12 +29,7 @@
*/
@Disabled
@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/522")
class JMockitDelegateToMockitoTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
setDefaultParserSettings(spec);
}
class JMockitDelegateToMockitoTest extends JMockitTestBase {

@DocumentExample
@Test
Original file line number Diff line number Diff line change
@@ -18,18 +18,10 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;
import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.setDefaultParserSettings;

class JMockitExpectationsToMockitoTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
setDefaultParserSettings(spec);
}
class JMockitExpectationsToMockitoTest extends JMockitTestBase {

@DocumentExample
@Test
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 static org.openrewrite.java.Assertions.java;

/**
* Not doing comprehensive testing as it is covered in JMockitVerificationsToMockitoTest and shares same code path
*/
class JMockitFullVerificationsToMockitoTest extends JMockitTestBase {

@DocumentExample
@Test
void whenMultipleMocks() {
//language=java
rewriteRun(
java(
"""
import mockit.FullVerifications;
import mockit.Mocked;
import mockit.integration.junit5.JMockitExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(JMockitExtension.class)
class MyTest {
@Mocked
Object myObject;
@Mocked
String str;
void test() {
myObject.wait(10L, 10);
myObject.wait(10L, 10);
str.notify();
new FullVerifications() {{
myObject.wait(anyLong, anyInt);
times = 2;
str.notify();
}};
}
}
""",
"""
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MyTest {
@Mock
Object myObject;
@Mock
String str;
void test() {
myObject.wait(10L, 10);
myObject.wait(10L, 10);
str.notify();
verify(myObject, times(2)).wait(anyLong(), anyInt());
verify(str).notify();
verifyNoMoreInteractions(myObject, str);
}
}
"""
)
);
}

@Test
void whenTimes() {
//language=java
rewriteRun(
java(
"""
import mockit.FullVerifications;
import mockit.Mocked;
import mockit.integration.junit5.JMockitExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(JMockitExtension.class)
class MyTest {
@Mocked
Object myObject;
void test() {
myObject.wait(10L, 10);
myObject.wait(10L, 10);
new FullVerifications() {{
myObject.wait(anyLong, anyInt);
times = 2;
}};
}
}
""",
"""
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MyTest {
@Mock
Object myObject;
void test() {
myObject.wait(10L, 10);
myObject.wait(10L, 10);
verify(myObject, times(2)).wait(anyLong(), anyInt());
verifyNoMoreInteractions(myObject);
}
}
"""
)
);
}

@DocumentExample
@Test
void whenOtherStatements() {
//language=java
rewriteRun(
java(
"""
import mockit.FullVerifications;
import mockit.Mocked;
import mockit.integration.junit5.JMockitExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(JMockitExtension.class)
class MyTest {
@Mocked
Object myObject;
void test() {
myObject.wait(10L, 10);
new FullVerifications() {{
myObject.wait(anyLong, anyInt);
}};
System.out.println("bla");
}
}
""",
"""
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MyTest {
@Mock
Object myObject;
void test() {
myObject.wait(10L, 10);
verify(myObject).wait(anyLong(), anyInt());
verifyNoMoreInteractions(myObject);
System.out.println("bla");
}
}
"""
)
);
}

@Test
void whenMultipleInvocationsSameMock() {
//language=java
rewriteRun(
java(
"""
import mockit.FullVerifications;
import mockit.Mocked;
import mockit.integration.junit5.JMockitExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(JMockitExtension.class)
class MyTest {
@Mocked
Object myObject;
void test() {
myObject.wait(10L, 10);
myObject.wait();
new FullVerifications() {{
myObject.wait(anyLong, anyInt);
myObject.wait();
}};
}
}
""",
"""
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MyTest {
@Mock
Object myObject;
void test() {
myObject.wait(10L, 10);
myObject.wait();
verify(myObject).wait(anyLong(), anyInt());
verify(myObject).wait();
verifyNoMoreInteractions(myObject);
}
}
"""
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.setDefaultParserSettings;

class JMockitTestBase implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
setDefaultParserSettings(spec);
}
}
Comment on lines +23 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to create a base class just to share a single method invocation's worth of configuration. Single inheritance of JMockitTestBase limits the ability of tests to extend other base classes for little code reuse. I would prefer to have setDefaultParserSettings() called explicitly in the defaults method of each individual test class.

Original file line number Diff line number Diff line change
@@ -17,18 +17,11 @@

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
import org.openrewrite.test.TypeValidation;

import static org.openrewrite.java.Assertions.java;
import static org.openrewrite.java.testing.jmockit.JMockitTestUtils.setDefaultParserSettings;

class JMockitVerificationsToMockitoTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
setDefaultParserSettings(spec);
}
class JMockitVerificationsToMockitoTest extends JMockitTestBase {

@DocumentExample
@Test
@@ -684,7 +677,7 @@ class MyTest {
void test() {
myObject.wait();
new Verifications() {{
new Verifications() {{
myObject.wait();
myObject.wait(anyLong, anyInt);
}};