Skip to content

Commit

Permalink
#24: Add support for strictness configuration when converting between…
Browse files Browse the repository at this point in the history
… the Mock annotation and Mockito.mock()
  • Loading branch information
picimako committed Jun 16, 2022
1 parent 1abeeb0 commit f54641a
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 28 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,9 @@
## [Unreleased]

## [0.5.0]
### Added
- Strictness configuration is now supported by the conversion between `@Mock` and `Mockito.mock(Class, MockSettings)` in both directions.

### Changed
- Removed support for IJ2021.1. From now on 2021.2 is the earliest version supported.

Expand Down
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -37,7 +37,8 @@ You can find the list of those inspections in the [Mockitools/SonarLint rules](d

## Why Mockitools is tasty

If [*"Mockito ... tastes really good"* and *"doesn’t give you hangover"*](https://site.mockito.org/#why), this IDE plugin will only make it better. You can find out why, in the dedicated documentation below.
If [*"Mockito ... tastes really good"* and *"doesn’t give you hangover"*](https://site.mockito.org/#why),
this IDE plugin will only make it better. You can find out why, in the dedicated documentation below.

- [Mock creation](docs/mock_creation.md)
- [Stubbing](docs/stubbing.md)
Expand Down
3 changes: 3 additions & 0 deletions docs/mock_creation.md
Expand Up @@ -318,6 +318,9 @@ to: @Mock Clazz clazz;

from: mock(Clazz.class, Mockito.withSettings().extraInterfaces(List.class))
to: @Mock(extraInterfaces = List.class) Clazz clazz;

from: mock(Clazz.class, Mockito.withSettings().strictness(Strictness.WARN))
to: @Mock(strictness = Mock.Strictness.WARN) Clazz clazz;
```

Furthermore, the type that is being mocked should be mockable either by Mockito's rules or not being annotated with `@DoNotMock`.
Expand Down
Binary file not shown.
Expand Up @@ -66,7 +66,7 @@ public final class MockitoolsPsiUtil {
*/
private static final Set<String> NON_MOCKABLE_TYPES = Set.of(CommonClassNames.JAVA_LANG_CLASS, CommonClassNames.JAVA_LANG_STRING);

private static final CallMatcher MOCKITO_MOCK = CallMatcher.staticCall(ORG_MOCKITO_MOCKITO, MOCK);
public static final CallMatcher.Simple MOCKITO_MOCK = CallMatcher.staticCall(ORG_MOCKITO_MOCKITO, MOCK);
private static final CallMatcher MOCKITO_SPY = CallMatcher.staticCall(ORG_MOCKITO_MOCKITO, SPY).parameterCount(1);
private static final CallMatcher BDDMOCKITO_GIVEN = CallMatcher.staticCall(ORG_MOCKITO_BDDMOCKITO, GIVEN).parameterCount(1);
private static final CallMatcher BDDMOCKITO_WILL_X =
Expand Down
Expand Up @@ -7,7 +7,6 @@
import static com.picimako.mockitools.MockitoQualifiedNames.DEFAULT_ANSWER;
import static com.picimako.mockitools.MockitoQualifiedNames.EXTRA_INTERFACES;
import static com.picimako.mockitools.MockitoQualifiedNames.LENIENT;
import static com.picimako.mockitools.MockitoQualifiedNames.MOCK;
import static com.picimako.mockitools.MockitoQualifiedNames.NAME;
import static com.picimako.mockitools.MockitoQualifiedNames.ORG_MOCKITO_ANSWER;
import static com.picimako.mockitools.MockitoQualifiedNames.ORG_MOCKITO_ANSWERS;
Expand All @@ -17,6 +16,7 @@
import static com.picimako.mockitools.MockitoQualifiedNames.ORG_MOCKITO_MOCK_SETTINGS;
import static com.picimako.mockitools.MockitoQualifiedNames.SERIALIZABLE;
import static com.picimako.mockitools.MockitoQualifiedNames.STUB_ONLY;
import static com.picimako.mockitools.MockitoolsPsiUtil.MOCKITO_MOCK;
import static com.picimako.mockitools.MockitoolsPsiUtil.isMockableTypeInAnyWay;
import static com.picimako.mockitools.PsiMethodUtil.collectCallsInChainFromLast;
import static com.picimako.mockitools.PsiMethodUtil.get2ndArgument;
Expand All @@ -31,10 +31,6 @@
import static com.siyeh.ig.psiutils.MethodCallUtils.getMethodName;
import static java.util.stream.Collectors.joining;

import java.util.Arrays;
import java.util.Set;
import java.util.function.Supplier;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
Expand All @@ -50,10 +46,14 @@
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.picimako.mockitools.MockitoQualifiedNames;
import com.picimako.mockitools.PsiClassUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import org.jetbrains.annotations.NotNull;

import com.picimako.mockitools.MockitoQualifiedNames;
import java.util.Arrays;
import java.util.Set;
import java.util.function.Supplier;

/**
* Converts {@code Mockito.mock()} calls to {@code @Mock} annotated fields.
Expand All @@ -64,6 +64,7 @@
* mock(Clazz.class, "some name") -> @Mock(name = "some name") Clazz clazz;
* mock(Clazz.class, Answers.RETURNS_SMART_NULLS) -> @Mock(answer = Answers.RETURNS_SMART_NULLS) Clazz clazz;
* mock(Clazz.class, Mockito.withSettings().lenient()) -> @Mock(lenient = true) Clazz clazz;
* mock(Clazz.class, Mockito.withSettings().strictness(Strictness.WARN)) -> @Mock(strictness = Mock.Strictness.WARN) Clazz clazz;
*
* Clazz localVar = mock(Clazz.class) -> @Mock Clazz localVar;
* Clazz&lt;typeargs> localVar = mock(Clazz.class) -> @Mock Clazz&lt;typeargs> localVar;
Expand All @@ -89,17 +90,17 @@
*/
public class ConvertMockCallToFieldIntention extends ConvertCallToFieldIntentionBase {
private static final String JAVA_LANG_CLASS = "java.lang.Class<T>";
private static final CallMatcher MOCKITO_MOCK = staticCall(ORG_MOCKITO_MOCKITO, MOCK).parameterTypes(JAVA_LANG_CLASS);
private static final CallMatcher MOCK_WITH_NAME = staticCall(ORG_MOCKITO_MOCKITO, MOCK).parameterTypes(JAVA_LANG_CLASS, CommonClassNames.JAVA_LANG_STRING);
private static final CallMatcher MOCK_WITH_ANSWER = staticCall(ORG_MOCKITO_MOCKITO, MOCK).parameterTypes(JAVA_LANG_CLASS, ORG_MOCKITO_ANSWER);
private static final CallMatcher MOCK_WITH_SETTINGS = staticCall(ORG_MOCKITO_MOCKITO, MOCK).parameterTypes(JAVA_LANG_CLASS, ORG_MOCKITO_MOCK_SETTINGS);
private static final CallMatcher MOCK = MOCKITO_MOCK.parameterTypes(JAVA_LANG_CLASS);
private static final CallMatcher MOCK_WITH_NAME = MOCKITO_MOCK.parameterTypes(JAVA_LANG_CLASS, CommonClassNames.JAVA_LANG_STRING);
private static final CallMatcher MOCK_WITH_ANSWER = MOCKITO_MOCK.parameterTypes(JAVA_LANG_CLASS, ORG_MOCKITO_ANSWER);
private static final CallMatcher MOCK_WITH_SETTINGS = MOCKITO_MOCK.parameterTypes(JAVA_LANG_CLASS, ORG_MOCKITO_MOCK_SETTINGS);
private static final CallMatcher MOCKITO_WITH_SETTINGS = staticCall(ORG_MOCKITO_MOCKITO, "withSettings");

private static final CallMatcher MOCK_SETTINGS_SERIALIZABLE_WITH_MODE = instanceCall(ORG_MOCKITO_MOCK_SETTINGS, SERIALIZABLE).parameterTypes(ORG_MOCKITO_MOCK_SERIALIZABLE_MODE);
private static final Set<String> SUPPORTED_MOCK_SETTINGS_METHODS = Set.of(DEFAULT_ANSWER, STUB_ONLY, NAME, EXTRA_INTERFACES, LENIENT);
private static final Set<String> SUPPORTED_MOCK_SETTINGS_METHODS = Set.of(DEFAULT_ANSWER, STUB_ONLY, NAME, EXTRA_INTERFACES, LENIENT, "strictness");

public ConvertMockCallToFieldIntention() {
super(MOCK, "@Mock");
super(MockitoQualifiedNames.MOCK, "@Mock");
}

//Availability
Expand All @@ -120,13 +121,16 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file
final PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
if (isIdentifierOfMethodCall(element)) {
var methodCall = (PsiMethodCallExpression) element.getParent().getParent();
if (!MOCK.equals(getMethodName(methodCall))) return false;
if (!MockitoQualifiedNames.MOCK.equals(getMethodName(methodCall))) return false;

var mockTypeArg = getFirstArgument(methodCall);
if (!(mockTypeArg instanceof PsiClassObjectAccessExpression) || !isMockableTypeInAnyWay(getOperandType(mockTypeArg))) return false;
if (MOCKITO_MOCK.matches(methodCall)) return hasOneArgument(methodCall);
if (MOCK_WITH_NAME.matches(methodCall) || MOCK_WITH_ANSWER.matches(methodCall)) return hasTwoArguments(methodCall);
if (MOCK_WITH_SETTINGS.matches(methodCall)) return hasTwoArguments(methodCall) && isSettingsSupportedByMockAnnotation(get2ndArgument(methodCall));
if (!(mockTypeArg instanceof PsiClassObjectAccessExpression) || !isMockableTypeInAnyWay(getOperandType(mockTypeArg)))
return false;
if (MOCK.matches(methodCall)) return hasOneArgument(methodCall);
if (MOCK_WITH_NAME.matches(methodCall) || MOCK_WITH_ANSWER.matches(methodCall))
return hasTwoArguments(methodCall);
if (MOCK_WITH_SETTINGS.matches(methodCall))
return hasTwoArguments(methodCall) && isSettingsSupportedByMockAnnotation(get2ndArgument(methodCall));
}
return false;
}
Expand Down Expand Up @@ -198,24 +202,27 @@ private void configureFromMockSettings(MockSettingsBasedAnnotationConfigurer con
for (int i = calls.size() - 2; i >= 0; i--) {
var call = calls.get(i);
String methodName = getMethodName(call);
if (STUB_ONLY.equals(methodName) || LENIENT.equals(methodName) || SERIALIZABLE.equals(methodName)) configurer.configureBooleanAttribute(methodName);
if (STUB_ONLY.equals(methodName) || LENIENT.equals(methodName) || SERIALIZABLE.equals(methodName))
configurer.configureBooleanAttribute(methodName);
else if (NAME.equals(methodName)) configurer.configureName(call);
else if (DEFAULT_ANSWER.equals(methodName)) configurer.configureAnswerFromCall(call);
else if (EXTRA_INTERFACES.equals(methodName)) configurer.configureExtraInterfaces(call);
else if ("strictness".equals(methodName)) configurer.configureStrictness(call);
}
}

/**
* Returns whether the argument Answer expression is a reference to {@code org.mockito.Answers.RETURNS_DEFAULTS}.
*/
public static boolean isDefaultAnswer(PsiExpression answer) {
if (answer instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) answer).resolve();
return resolved instanceof PsiEnumConstant
&& ORG_MOCKITO_ANSWERS.equals(((PsiEnumConstant) resolved).getContainingClass().getQualifiedName())
&& "RETURNS_DEFAULTS".equals(((PsiEnumConstant) resolved).getName());
}
return false;
return answer instanceof PsiReferenceExpression
&& isEnumConstant(((PsiReferenceExpression) answer).resolve(), ORG_MOCKITO_ANSWERS, "RETURNS_DEFAULTS");
}

private static boolean isEnumConstant(PsiElement constant, String enumClassName, String enumConstantName) {
return constant instanceof PsiEnumConstant
&& enumClassName.equals(((PsiEnumConstant) constant).getContainingClass().getQualifiedName())
&& enumConstantName.equals(((PsiEnumConstant) constant).getName());
}

/**
Expand Down Expand Up @@ -262,6 +269,20 @@ private void configureExtraInterfaces(PsiMethodCallExpression call) {
}
}

private void configureStrictness(PsiMethodCallExpression call) {
var strictness = getFirstArgument(call);
//null value passed into MockSettings.strictness() is not handled since it is invalid anyway.
if (strictness instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) strictness).resolve();
if (resolved instanceof PsiEnumConstant) {
PsiEnumConstant constant = (PsiEnumConstant) resolved;
String strictnessName = constant.getName();
PsiClassUtil.importClass("org.mockito.Mock.Strictness", mockAnnotation);
mockAnnotation.setDeclaredAttributeValue("strictness", attributeValue("Mock.Strictness." + strictnessName));
}
}
}

@NotNull
private PsiExpression attributeValue(String text) {
return JavaPsiFacade.getElementFactory(project).createExpressionFromText(text, mockAnnotation);
Expand Down
Expand Up @@ -23,7 +23,7 @@ public static void loadMockito3(@NotNull Disposable projectDisposable, @NotNull
}

public static void loadMockito4Latest(@NotNull Disposable projectDisposable, @NotNull Module module) {
loadLibrary(projectDisposable, module, "Mockito 4 Library", "mockito-core-4.2.0.jar");
loadLibrary(projectDisposable, module, "Mockito 4 Library", "mockito-core-4.6.1.jar");
}

public static void loadJUnit4(@NotNull Disposable projectDisposable, @NotNull Module module) {
Expand Down
Expand Up @@ -439,6 +439,32 @@ public void testConvertsMockitoMockWithSettingsNameFromVariable() {
"}");
}

public void testConvertsMockitoMockWithSettingsStrictness() {
checkIntentionRun(
"import org.mockito.Mockito;\n" +
"import org.mockito.quality.Strictness;\n" +
"\n" +
"public class ConversionTest {\n" +
" public void testMethod() {\n" +
" aMethod(Mockito.mo<caret>ck(Object.class, Mockito.withSettings().strictness(Strictness.WARN)));\n" +
" }\n" +
" public void aMethod(Object object) { }\n" +
"}",
"import org.mockito.Mock;\n" +
"import org.mockito.Mockito;\n" +
"import org.mockito.quality.Strictness;\n" +
"\n" +
"public class ConversionTest {\n" +
" @Mock(strictness = Mock.Strictness.WARN)\n" +
" Object object;\n" +
"\n" +
" public void testMethod() {\n" +
" aMethod(object);\n" +
" }\n" +
" public void aMethod(Object object) { }\n" +
"}");
}

public void testConvertsMockitoMockWithSettingsDefaultAnswer() {
checkIntentionRun(
"import org.mockito.Mockito;\n" +
Expand Down

0 comments on commit f54641a

Please sign in to comment.