Skip to content

Commit

Permalink
Add recipe for replacing unnecesary Mockito#eq with direct parameters (
Browse files Browse the repository at this point in the history
…#615)

* Add recipe for unnecesary Mockito#eq

This adds a basic recipe for replacing Mockito#eq with direct parameters in when this is supported.
This change should resolve Sonar issue java:S6068

* Apply suggestions from code review

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Drop unnecessary annotations

* Rename class and apply some first best practices

* Make the tests pass by retaining the prefix

* Update recipe to also support `doThrow#when` and `BDDMockito#given`

Also tests methods using Method Matchers.
Also removes the `Mockito#eq` import if it is not required anymore.

* Format code

* Apply suggested formatting changes

* Apply suggested formatting changes

* Inline methods simplified through MethodMatcher

* Cast to MethodCall & replace conditionally

* Remove unused import

* Collapse if's with similar blocks

* Add SimplifyMockitoVerifyWhenGiven to MockitoBestPractices

---------

Co-authored-by: Tim te Beek <timtebeek@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tim te Beek <tim@moderne.io>
  • Loading branch information
4 people authored Oct 13, 2024
1 parent 7bbd13a commit 3c92c0f
Show file tree
Hide file tree
Showing 3 changed files with 359 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2024 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.mockito;

import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.MethodCall;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class SimplifyMockitoVerifyWhenGiven extends Recipe {

private static final MethodMatcher WHEN_MATCHER = new MethodMatcher("org.mockito.Mockito when(..)");
private static final MethodMatcher GIVEN_MATCHER = new MethodMatcher("org.mockito.BDDMockito given(..)");
private static final MethodMatcher VERIFY_MATCHER = new MethodMatcher("org.mockito.Mockito verify(..)");
private static final MethodMatcher STUBBER_MATCHER = new MethodMatcher("org.mockito.stubbing.Stubber when(..)");
private static final MethodMatcher EQ_MATCHER = new MethodMatcher("org.mockito.ArgumentMatchers eq(..)");

@Override
public String getDisplayName() {
return "Call to Mockito method \"verify\", \"when\" or \"given\" should be simplified";
}

@Override
public String getDescription() {
return "Fixes Sonar issue `java:S6068`: Call to Mockito method \"verify\", \"when\" or \"given\" should be simplified.";
}

@Override
public Set<String> getTags() {
return Collections.singleton("RSPEC-6068");
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesMethod<>(EQ_MATCHER), new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation methodInvocation, ExecutionContext ctx) {
J.MethodInvocation mi = super.visitMethodInvocation(methodInvocation, ctx);

if ((WHEN_MATCHER.matches(mi) || GIVEN_MATCHER.matches(mi)) && mi.getArguments().get(0) instanceof J.MethodInvocation) {
List<Expression> updatedArguments = new ArrayList<>(mi.getArguments());
updatedArguments.set(0, checkAndUpdateEq((J.MethodInvocation) mi.getArguments().get(0)));
mi = mi.withArguments(updatedArguments);
} else if (VERIFY_MATCHER.matches(mi.getSelect()) ||
STUBBER_MATCHER.matches(mi.getSelect())) {
mi = checkAndUpdateEq(mi);
}

maybeRemoveImport("org.mockito.ArgumentMatchers.eq");
return mi;
}

private J.MethodInvocation checkAndUpdateEq(J.MethodInvocation methodInvocation) {
if (methodInvocation.getArguments().stream().allMatch(EQ_MATCHER::matches)) {
return methodInvocation.withArguments(ListUtils.map(methodInvocation.getArguments(), invocation ->
((MethodCall) invocation).getArguments().get(0).withPrefix(invocation.getPrefix())));
}
return methodInvocation;
}
});
}

}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/rewrite/mockito.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ recipeList:
- org.openrewrite.java.testing.mockito.Mockito1to5Migration
- org.openrewrite.java.RemoveAnnotation:
annotationPattern: "@org.mockito.junit.jupiter.MockitoSettings(strictness=org.mockito.quality.Strictness.WARN)"
- org.openrewrite.java.testing.mockito.SimplifyMockitoVerifyWhenGiven
---
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.java.testing.mockito.Mockito1to5Migration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* Copyright 2024 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.mockito;

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 SimplifyMockitoVerifyWhenGivenTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new SimplifyMockitoVerifyWhenGiven())
.parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(), "mockito-core"));
}

@DocumentExample
@Test
void shouldRemoveUnneccesaryEqFromVerify() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.mock;
import static org.mockito.ArgumentMatchers.eq;
class Test {
void test() {
var mockString = mock(String.class);
verify(mockString).replace(eq("foo"), eq("bar"));
}
}
""", """
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.mock;
class Test {
void test() {
var mockString = mock(String.class);
verify(mockString).replace("foo", "bar");
}
}
"""
)
);
}

@Test
void shouldRemoveUnneccesaryEqFromWhen() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.eq;
class Test {
void test() {
var mockString = mock(String.class);
when(mockString.replace(eq("foo"), eq("bar"))).thenReturn("bar");
}
}
""", """
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class Test {
void test() {
var mockString = mock(String.class);
when(mockString.replace("foo", "bar")).thenReturn("bar");
}
}
"""
)
);
}

@Test
void shouldNotRemoveEqWhenMatchersAreMixed() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyString;
class Test {
void test() {
var mockString = mock(String.class);
when(mockString.replace(eq("foo"), anyString())).thenReturn("bar");
}
}
"""
)
);
}

@Test
void shouldRemoveUnneccesaryEqFromStubber() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.Mockito.doThrow;
import static org.mockito.ArgumentMatchers.eq;
class Test {
void test() {
doThrow(new RuntimeException()).when("foo").substring(eq(1));
}
}
""", """
import static org.mockito.Mockito.doThrow;
class Test {
void test() {
doThrow(new RuntimeException()).when("foo").substring(1);
}
}
"""
)
);
}

@Test
void shouldRemoveUnneccesaryEqFromBDDGiven() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.BDDMockito.given;
import static org.mockito.ArgumentMatchers.eq;
class Test {
void test() {
given("foo".substring(eq(1)));
}
}
""", """
import static org.mockito.BDDMockito.given;
class Test {
void test() {
given("foo".substring(1));
}
}
"""
)
);
}

@Test
void shouldNotRemoveEqImportWhenStillNeeded() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyString;
class Test {
void testRemoveEq() {
var mockString = mock(String.class);
when(mockString.replace(eq("foo"), eq("bar"))).thenReturn("bar");
}
void testKeepEq() {
var mockString = mock(String.class);
when(mockString.replace(eq("foo"), anyString())).thenReturn("bar");
}
}
""", """
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyString;
class Test {
void testRemoveEq() {
var mockString = mock(String.class);
when(mockString.replace("foo", "bar")).thenReturn("bar");
}
void testKeepEq() {
var mockString = mock(String.class);
when(mockString.replace(eq("foo"), anyString())).thenReturn("bar");
}
}
"""
)
);
}

@Test
void shouldFixSonarExamples() {
rewriteRun(
//language=Java
java(
"""
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.doThrow;
import static org.mockito.BDDMockito.given;
import static org.mockito.ArgumentMatchers.eq;
class Test {
void test(Object v1, Object v2, Object v3, Object v4, Object v5, Foo foo) {
given(foo.bar(eq(v1), eq(v2), eq(v3))).willReturn(null);
when(foo.baz(eq(v4), eq(v5))).thenReturn("foo");
doThrow(new RuntimeException()).when(foo).quux(eq(42));
verify(foo).bar(eq(v1), eq(v2), eq(v3));
}
}
class Foo {
Object bar(Object v1, Object v2, Object v3) { return null; }
String baz(Object v4, Object v5) { return ""; }
void quux(int x) {}
}
""", """
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.doThrow;
import static org.mockito.BDDMockito.given;
class Test {
void test(Object v1, Object v2, Object v3, Object v4, Object v5, Foo foo) {
given(foo.bar(v1, v2, v3)).willReturn(null);
when(foo.baz(v4, v5)).thenReturn("foo");
doThrow(new RuntimeException()).when(foo).quux(42);
verify(foo).bar(v1, v2, v3);
}
}
class Foo {
Object bar(Object v1, Object v2, Object v3) { return null; }
String baz(Object v4, Object v5) { return ""; }
void quux(int x) {}
}
"""
)
);
}
}

0 comments on commit 3c92c0f

Please sign in to comment.