diff --git a/src/main/java/org/mockito/Mock.java b/src/main/java/org/mockito/Mock.java
index 2473557e56..36caf4f189 100644
--- a/src/main/java/org/mockito/Mock.java
+++ b/src/main/java/org/mockito/Mock.java
@@ -23,6 +23,7 @@
*
Minimizes repetitive mock creation code.
* Makes the test class more readable.
* Makes the verification error easier to read because the field name is used to identify the mock.
+ * Automatically detects static mocks of type {@link MockedStatic} and infers the static mock type of the type parameter.
*
*
*
diff --git a/src/main/java/org/mockito/MockSettings.java b/src/main/java/org/mockito/MockSettings.java
index 30496d164c..a1fdb508fd 100644
--- a/src/main/java/org/mockito/MockSettings.java
+++ b/src/main/java/org/mockito/MockSettings.java
@@ -340,6 +340,21 @@ public interface MockSettings extends Serializable {
@Incubating
MockCreationSettings build(Class typeToMock);
+ /**
+ * Creates immutable view of mock settings used later by Mockito, for use within a static mocking.
+ * Framework integrators can use this method to create instances of creation settings
+ * and use them in advanced use cases, for example to create invocations with {@link InvocationFactory},
+ * or to implement custom {@link MockHandler}.
+ * Since {@link MockCreationSettings} is {@link NotExtensible}, Mockito public API needs a creation method for this type.
+ *
+ * @param classToMock class to mock
+ * @param type to mock
+ * @return immutable view of mock settings
+ * @since 2.10.0
+ */
+ @Incubating
+ MockCreationSettings buildStatic(Class classToMock);
+
/**
* Lenient mocks bypass "strict stubbing" validation (see {@link Strictness#STRICT_STUBS}).
* When mock is declared as lenient none of its stubbings will be checked for potential stubbing problems such as
diff --git a/src/main/java/org/mockito/MockedStatic.java b/src/main/java/org/mockito/MockedStatic.java
new file mode 100644
index 0000000000..3ec0acfad1
--- /dev/null
+++ b/src/main/java/org/mockito/MockedStatic.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2007 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito;
+
+import org.mockito.exceptions.base.MockitoAssertionError;
+import org.mockito.exceptions.base.MockitoException;
+import org.mockito.internal.creation.StaticMockControl;
+import org.mockito.internal.debugging.LocationImpl;
+import org.mockito.internal.listeners.VerificationStartedNotifier;
+import org.mockito.internal.progress.MockingProgress;
+import org.mockito.internal.stubbing.InvocationContainerImpl;
+import org.mockito.internal.verification.MockAwareVerificationMode;
+import org.mockito.internal.verification.VerificationDataImpl;
+import org.mockito.invocation.Location;
+import org.mockito.invocation.MockHandler;
+import org.mockito.stubbing.OngoingStubbing;
+import org.mockito.verification.VerificationMode;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.internal.exceptions.Reporter.missingMethodInvocation;
+import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
+import static org.mockito.internal.util.MockUtil.getInvocationContainer;
+import static org.mockito.internal.util.MockUtil.resetMock;
+import static org.mockito.internal.util.StringUtil.join;
+import static org.mockito.internal.verification.VerificationModeFactory.noInteractions;
+import static org.mockito.internal.verification.VerificationModeFactory.noMoreInteractions;
+
+@Incubating
+public class MockedStatic implements AutoCloseable {
+
+ private final StaticMockControl control;
+
+ private boolean closed;
+
+ private final Location location = new LocationImpl();
+
+ public MockedStatic(StaticMockControl control) {
+ this.control = control;
+ }
+
+ public OngoingStubbing when(Verification verification) {
+ assertNotClosed();
+
+ try {
+ verification.apply();
+ } catch (Throwable ignored) { }
+
+ MockingProgress mockingProgress = mockingProgress();
+ mockingProgress.stubbingStarted();
+ @SuppressWarnings("unchecked")
+ OngoingStubbing stubbing = (OngoingStubbing) mockingProgress.pullOngoingStubbing();
+ if (stubbing == null) {
+ mockingProgress.reset();
+ throw missingMethodInvocation();
+ }
+ return stubbing;
+ }
+
+ public void verify(Verification verification) {
+ verify(times(1), verification);
+ }
+
+ public void verify(VerificationMode mode, Verification verification) {
+ assertNotClosed();
+
+ MockingDetails mockingDetails = Mockito.mockingDetails(control.getType());
+ MockHandler handler = mockingDetails.getMockHandler();
+
+ VerificationStartedNotifier.notifyVerificationStarted(
+ handler.getMockSettings().getVerificationStartedListeners(),
+ mockingDetails);
+
+ MockingProgress mockingProgress = mockingProgress();
+ VerificationMode actualMode = mockingProgress.maybeVerifyLazily(mode);
+ mockingProgress.verificationStarted(
+ new MockAwareVerificationMode(
+ control.getType(), actualMode, mockingProgress.verificationListeners()));
+
+ try {
+ verification.apply();
+ } catch (MockitoException | MockitoAssertionError e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new MockitoException(join(
+ "An unexpected error occurred while verifying a static stub",
+ "",
+ "To correctly verify a stub, invoke a single static method of " + control.getType().getName() + " in the provided lambda.",
+ "For example, if a method 'sample' was defined, provide a lambda or anonymous class containing the code",
+ "",
+ "() -> " + control.getType().getSimpleName() + ".sample()",
+ "or",
+ control.getType().getSimpleName() + "::sample"
+ ), t);
+ }
+ }
+
+ public void reset() {
+ assertNotClosed();
+
+ MockingProgress mockingProgress = mockingProgress();
+ mockingProgress.validateState();
+ mockingProgress.reset();
+ mockingProgress.resetOngoingStubbing();
+
+ resetMock(control.getType());
+ }
+
+ public void clearInvocations() {
+ assertNotClosed();
+
+ MockingProgress mockingProgress = mockingProgress();
+ mockingProgress.validateState();
+ mockingProgress.reset();
+ mockingProgress.resetOngoingStubbing();
+
+ getInvocationContainer(control.getType()).clearInvocations();
+ }
+
+ public void verifyNoMoreInteractions() {
+ assertNotClosed();
+
+ mockingProgress().validateState();
+ InvocationContainerImpl invocations = getInvocationContainer(control.getType());
+ VerificationDataImpl data = new VerificationDataImpl(invocations, null);
+ noMoreInteractions().verify(data);
+ }
+
+ public void verifyNoInteractions() {
+ assertNotClosed();
+
+ mockingProgress().validateState();
+ InvocationContainerImpl invocations = getInvocationContainer(control.getType());
+ VerificationDataImpl data = new VerificationDataImpl(invocations, null);
+ noInteractions().verify(data);
+ }
+
+ @Override
+ public void close() {
+ assertNotClosed();
+
+ closed = true;
+ control.disable();
+ }
+
+ public void closeOnDemand() {
+ if (!closed) {
+ close();
+ }
+ }
+
+ private void assertNotClosed() {
+ if (closed) {
+ throw new MockitoException(join(
+ "The static mock created at",
+ location.toString(),
+ "is already resolved and cannot longer be used"
+ ));
+ }
+ }
+
+ @FunctionalInterface
+ public interface Verification {
+
+ void apply() throws Throwable;
+ }
+}
diff --git a/src/main/java/org/mockito/Mockito.java b/src/main/java/org/mockito/Mockito.java
index 07a5dfcb56..bf34c148f3 100644
--- a/src/main/java/org/mockito/Mockito.java
+++ b/src/main/java/org/mockito/Mockito.java
@@ -105,6 +105,7 @@
* 45. New JUnit Jupiter (JUnit5+) extension
* 46. New Mockito.lenient()
and MockSettings.lenient()
methods (Since 2.20.0)
* 47. New API for clearing mock state in inline mocking (Since 2.25.0)
+ * 48. New API for mocking static methods (Since 3.4.0)
*
*
*
@@ -1541,6 +1542,29 @@
* Hence, we introduced a new API to explicitly clear mock state (only make sense in inline mocking!).
* See example usage in {@link MockitoFramework#clearInlineMocks()}.
* If you have feedback or a better idea how to solve the problem please reach out.
+ *
+ *
+ *
+ *
+ * When using the inline mock maker , it is possible to mock static method invocations within the current
+ * thread and a user-defined scope. This way, Mockito assures that concurrently and sequentially running tests do not interfere.
+ *
+ * To make sure a static mock remains temporary, it is recommended to define the scope within a try-with-resources construct.
+ * In the following example, the Foo
type's static method would return foo
unless mocked:
+ *
+ *
+ assertEquals("foo", Foo.method());
+ try (MockedStatic mocked = mockStatic(Foo.class)) {
+ mocked.when(Foo::method).thenReturn("bar");
+ assertEquals("bar", Foo.method());
+ mocked.verify(Foo::method);
+ }
+ assertEquals("foo", Foo.method());
+ *
+ *
+ * Due to the defined scope of the static mock, it returns to its original behvior once the scope is released. To define mock
+ * behavior and to verify static method invocations, use the MockedStatic
that is returned.
+ *
*/
@SuppressWarnings("unchecked")
public class Mockito extends ArgumentMatchers {
@@ -2026,6 +2050,73 @@ public static T spy(Class classToSpy) {
classToSpy, withSettings().useConstructor().defaultAnswer(CALLS_REAL_METHODS));
}
+ /**
+ * Creates a thread-local mock controller for all static methods of the given class or interface.
+ * The returned object's {@link MockedStatic#close()} method must be called upon completing the
+ * test or the mock will remain active on the current thread.
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ * @param classToMock class or interface of which static mocks should be mocked.
+ * @return mock controller
+ */
+ @Incubating
+ @CheckReturnValue
+ public static MockedStatic mockStatic(Class classToMock) {
+ return mockStatic(classToMock, withSettings());
+ }
+
+ /**
+ * Creates a thread-local mock controller for all static methods of the given class or interface.
+ * The returned object's {@link MockedStatic#close()} method must be called upon completing the
+ * test or the mock will remain active on the current thread.
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ * @param classToMock class or interface of which static mocks should be mocked.
+ * @param defaultAnswer the default answer when invoking static methods.
+ * @return mock controller
+ */
+ @Incubating
+ @CheckReturnValue
+ public static MockedStatic mockStatic(Class classToMock, Answer defaultAnswer) {
+ return mockStatic(classToMock, withSettings().defaultAnswer(defaultAnswer));
+ }
+
+ /**
+ * Creates a thread-local mock controller for all static methods of the given class or interface.
+ * The returned object's {@link MockedStatic#close()} method must be called upon completing the
+ * test or the mock will remain active on the current thread.
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ * @param classToMock class or interface of which static mocks should be mocked.
+ * @param name the name of the mock to use in error messages.
+ * @return mock controller
+ */
+ @Incubating
+ @CheckReturnValue
+ public static MockedStatic mockStatic(Class classToMock, String name) {
+ return mockStatic(classToMock, withSettings().name(name));
+ }
+
+ /**
+ * Creates a thread-local mock controller for all static methods of the given class or interface.
+ * The returned object's {@link MockedStatic#close()} method must be called upon completing the
+ * test or the mock will remain active on the current thread.
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ * @param classToMock class or interface of which static mocks should be mocked.
+ * @param mockSettings the settings to use where only name and default answer are considered.
+ * @return mock controller
+ */
+ @Incubating
+ @CheckReturnValue
+ public static MockedStatic mockStatic(Class classToMock, MockSettings mockSettings) {
+ return MOCKITO_CORE.mockStatic(classToMock, mockSettings);
+ }
+
/**
* Enables stubbing methods. Use it when you want the mock to return particular value when particular method is called.
*
diff --git a/src/main/java/org/mockito/MockitoAnnotations.java b/src/main/java/org/mockito/MockitoAnnotations.java
index ba355e702f..e6518797f3 100644
--- a/src/main/java/org/mockito/MockitoAnnotations.java
+++ b/src/main/java/org/mockito/MockitoAnnotations.java
@@ -37,8 +37,14 @@
*
* public class SampleBaseTestCase {
*
+ * private AutoCloseable closeable;
+ *
* @Before public void initMocks() {
- * MockitoAnnotations.initMocks(this);
+ * closeable = MockitoAnnotations.initMocks(this);
+ * }
+ *
+ * @After public void releaseMocks() throws Exception {
+ * closeable.close();
* }
* }
*
@@ -49,7 +55,8 @@
*
* In above example, initMocks()
is called in @Before (JUnit4) method of test's base class.
* For JUnit3 initMocks()
can go to setup()
method of a base class.
- * You can also put initMocks() in your JUnit runner (@RunWith) or use built-in runner: {@link MockitoJUnitRunner}
+ * You can also put initMocks() in your JUnit runner (@RunWith) or use built-in runner: {@link MockitoJUnitRunner}.
+ * If static method mocks are used, it is required to close the initialization.
*/
public class MockitoAnnotations {
@@ -59,7 +66,7 @@ public class MockitoAnnotations {
*
* See examples in javadoc for {@link MockitoAnnotations} class.
*/
- public static void initMocks(Object testClass) {
+ public static AutoCloseable initMocks(Object testClass) {
if (testClass == null) {
throw new MockitoException(
"testClass cannot be null. For info how to use @Mock annotations see examples in javadoc for MockitoAnnotations class");
@@ -67,6 +74,6 @@ public static void initMocks(Object testClass) {
AnnotationEngine annotationEngine =
new GlobalConfiguration().tryGetPluginAnnotationEngine();
- annotationEngine.process(testClass.getClass(), testClass);
+ return annotationEngine.process(testClass.getClass(), testClass);
}
}
diff --git a/src/main/java/org/mockito/internal/MockitoCore.java b/src/main/java/org/mockito/internal/MockitoCore.java
index 4353e0fbda..d36dfc1db1 100644
--- a/src/main/java/org/mockito/internal/MockitoCore.java
+++ b/src/main/java/org/mockito/internal/MockitoCore.java
@@ -7,6 +7,7 @@
import static org.mockito.internal.exceptions.Reporter.*;
import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
import static org.mockito.internal.util.MockUtil.createMock;
+import static org.mockito.internal.util.MockUtil.createStaticMock;
import static org.mockito.internal.util.MockUtil.getInvocationContainer;
import static org.mockito.internal.util.MockUtil.getMockHandler;
import static org.mockito.internal.util.MockUtil.isMock;
@@ -20,9 +21,11 @@
import org.mockito.InOrder;
import org.mockito.MockSettings;
+import org.mockito.MockedStatic;
import org.mockito.MockingDetails;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.internal.creation.MockSettingsImpl;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.internal.invocation.finder.VerifiableInvocationsFinder;
import org.mockito.internal.listeners.VerificationStartedNotifier;
import org.mockito.internal.progress.MockingProgress;
@@ -68,6 +71,22 @@ public T mock(Class typeToMock, MockSettings settings) {
return mock;
}
+ public MockedStatic mockStatic(Class classToMock, MockSettings settings) {
+ if (!MockSettingsImpl.class.isInstance(settings)) {
+ throw new IllegalArgumentException(
+ "Unexpected implementation of '"
+ + settings.getClass().getCanonicalName()
+ + "'\n"
+ + "At the moment, you cannot provide your own implementations of that class.");
+ }
+ MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
+ MockCreationSettings creationSettings = impl.buildStatic(classToMock);
+ StaticMockControl control = createStaticMock(classToMock, creationSettings);
+ control.enable();
+ mockingProgress().mockingStarted(classToMock, creationSettings);
+ return new MockedStatic<>(control);
+ }
+
public OngoingStubbing when(T methodCall) {
MockingProgress mockingProgress = mockingProgress();
mockingProgress.stubbingStarted();
diff --git a/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java
index 9b1c3767d0..24cb7a1cb2 100644
--- a/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java
+++ b/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java
@@ -9,11 +9,14 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.mockito.Captor;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.plugins.AnnotationEngine;
@@ -60,18 +63,25 @@ private void registerAnnotationProcessor(
}
@Override
- public void process(Class> clazz, Object testInstance) {
+ public AutoCloseable process(Class> clazz, Object testInstance) {
+ List> mockedStatics = new ArrayList<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
boolean alreadyAssigned = false;
for (Annotation annotation : field.getAnnotations()) {
Object mock = createMockFor(annotation, field);
+ if (mock instanceof MockedStatic>) {
+ mockedStatics.add((MockedStatic>) mock);
+ }
if (mock != null) {
throwIfAlreadyAssigned(field, alreadyAssigned);
alreadyAssigned = true;
try {
setField(testInstance, field, mock);
} catch (Exception e) {
+ for (MockedStatic> mockedStatic : mockedStatics) {
+ mockedStatic.close();
+ }
throw new MockitoException(
"Problems setting field "
+ field.getName()
@@ -82,6 +92,11 @@ public void process(Class> clazz, Object testInstance) {
}
}
}
+ return () -> {
+ for (MockedStatic> mockedStatic : mockedStatics) {
+ mockedStatic.closeOnDemand();
+ }
+ };
}
void throwIfAlreadyAssigned(Field field, boolean alreadyAssigned) {
diff --git a/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java
index ccc7ae4d85..2cbbadc362 100644
--- a/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java
+++ b/src/main/java/org/mockito/internal/configuration/InjectingAnnotationEngine.java
@@ -7,9 +7,12 @@
import static org.mockito.internal.util.collections.Sets.newMockSafeHashSet;
import java.lang.reflect.Field;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
+import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import org.mockito.internal.configuration.injection.scanner.InjectMocksScanner;
import org.mockito.internal.configuration.injection.scanner.MockScanner;
@@ -39,29 +42,39 @@ public class InjectingAnnotationEngine
*
* @see org.mockito.plugins.AnnotationEngine#process(Class, Object)
*/
- public void process(Class> clazz, Object testInstance) {
- processIndependentAnnotations(testInstance.getClass(), testInstance);
- processInjectMocks(testInstance.getClass(), testInstance);
+ public AutoCloseable process(Class> clazz, Object testInstance) {
+ List closeables = new ArrayList<>();
+ closeables.addAll(processIndependentAnnotations(testInstance.getClass(), testInstance));
+ closeables.addAll(processInjectMocks(testInstance.getClass(), testInstance));
+ return () -> {
+ for (AutoCloseable closeable : closeables) {
+ closeable.close();
+ }
+ };
}
- private void processInjectMocks(final Class> clazz, final Object testInstance) {
+ private List processInjectMocks(final Class> clazz, final Object testInstance) {
+ List closeables = new ArrayList<>();
Class> classContext = clazz;
while (classContext != Object.class) {
- injectMocks(testInstance);
+ closeables.add(injectMocks(testInstance));
classContext = classContext.getSuperclass();
}
+ return closeables;
}
- private void processIndependentAnnotations(final Class> clazz, final Object testInstance) {
+ private List processIndependentAnnotations(final Class> clazz, final Object testInstance) {
+ List closeables = new ArrayList<>();
Class> classContext = clazz;
while (classContext != Object.class) {
// this will create @Mocks, @Captors, etc:
- delegate.process(classContext, testInstance);
+ closeables.add(delegate.process(classContext, testInstance));
// this will create @Spies:
- spyAnnotationEngine.process(classContext, testInstance);
+ closeables.add(spyAnnotationEngine.process(classContext, testInstance));
classContext = classContext.getSuperclass();
}
+ return closeables;
}
/**
@@ -73,7 +86,7 @@ private void processIndependentAnnotations(final Class> clazz, final Object te
* @param testClassInstance
* Test class, usually this
*/
- public void injectMocks(final Object testClassInstance) {
+ private AutoCloseable injectMocks(final Object testClassInstance) {
Class> clazz = testClassInstance.getClass();
Set mockDependentFields = new HashSet();
Set mocks = newMockSafeHashSet();
@@ -87,6 +100,14 @@ public void injectMocks(final Object testClassInstance) {
new DefaultInjectionEngine()
.injectMocksOnFields(mockDependentFields, mocks, testClassInstance);
+
+ return () -> {
+ for (Object mock : mocks) {
+ if (mock instanceof MockedStatic>) {
+ ((MockedStatic>) mock).closeOnDemand();
+ }
+ }
+ };
}
protected void onInjection(
diff --git a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java
index 2994f1c84e..46e132221b 100644
--- a/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java
+++ b/src/main/java/org/mockito/internal/configuration/MockAnnotationProcessor.java
@@ -4,11 +4,18 @@
*/
package org.mockito.internal.configuration;
+import static org.mockito.internal.util.StringUtil.join;
+
import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
import org.mockito.Mock;
import org.mockito.MockSettings;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import org.mockito.exceptions.base.MockitoException;
+import org.mockito.internal.util.Supplier;
/**
* Instantiates a mock on a field annotated by {@link Mock}
@@ -16,10 +23,10 @@
public class MockAnnotationProcessor implements FieldAnnotationProcessor {
@Override
public Object process(Mock annotation, Field field) {
- return processAnnotationForMock(annotation, field.getType(), field.getName());
+ return processAnnotationForMock(annotation, field.getType(), field::getGenericType, field.getName());
}
- public static Object processAnnotationForMock(Mock annotation, Class> type, String name) {
+ public static Object processAnnotationForMock(Mock annotation, Class> type, Supplier genericType, String name) {
MockSettings mockSettings = Mockito.withSettings();
if (annotation.extraInterfaces().length > 0) { // never null
mockSettings.extraInterfaces(annotation.extraInterfaces());
@@ -41,6 +48,29 @@ public static Object processAnnotationForMock(Mock annotation, Class> type, St
// see @Mock answer default value
mockSettings.defaultAnswer(annotation.answer());
- return Mockito.mock(type, mockSettings);
+
+ if (type == MockedStatic.class) {
+ return Mockito.mockStatic(inferStaticMock(genericType.get(), name), mockSettings);
+ } else {
+ return Mockito.mock(type, mockSettings);
+ }
+ }
+
+ private static Class> inferStaticMock(Type type, String name) {
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ return (Class>) parameterizedType.getRawType();
+ } else {
+ throw new MockitoException(join(
+ "Mockito cannot infer a static mock from a raw type for " + name,
+ "",
+ "Instead of @Mock MockedStatic you need to specify a parameterized type",
+ "For example, if you would like to mock static methods of Sample.class, specify",
+ "",
+ "@Mock MockedStatic",
+ "",
+ "as the type parameter"
+ ));
+ }
}
}
diff --git a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java
index 27ebc4a04d..04fd4cae05 100644
--- a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java
+++ b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java
@@ -48,7 +48,7 @@ public class SpyAnnotationEngine
implements AnnotationEngine, org.mockito.configuration.AnnotationEngine {
@Override
- public void process(Class> context, Object testInstance) {
+ public AutoCloseable process(Class> context, Object testInstance) {
Field[] fields = context.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Spy.class)
@@ -78,6 +78,7 @@ public void process(Class> context, Object testInstance) {
}
}
}
+ return new NoAction();
}
private static Object spyInstance(Field field, Object instance) {
diff --git a/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java b/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java
index dee7f45c06..e3e1b4d9b9 100644
--- a/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java
+++ b/src/main/java/org/mockito/internal/creation/MockSettingsImpl.java
@@ -21,6 +21,7 @@
import java.util.Set;
import org.mockito.MockSettings;
+import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.creation.settings.CreationSettings;
import org.mockito.internal.debugging.VerboseMockInvocationLogger;
import org.mockito.internal.util.Checks;
@@ -231,6 +232,11 @@ public MockCreationSettings build(Class typeToMock) {
return validatedSettings(typeToMock, (CreationSettings) this);
}
+ @Override
+ public MockCreationSettings buildStatic(Class classToMock) {
+ return validatedStaticSettings(classToMock, (CreationSettings) this);
+ }
+
@Override
public MockSettings lenient() {
this.lenient = true;
@@ -254,12 +260,31 @@ private static CreationSettings validatedSettings(
// TODO do we really need to copy the entire settings every time we create mock object? it
// does not seem necessary.
CreationSettings settings = new CreationSettings(source);
- settings.setMockName(new MockNameImpl(source.getName(), typeToMock));
+ settings.setMockName(new MockNameImpl(source.getName(), typeToMock, false));
settings.setTypeToMock(typeToMock);
settings.setExtraInterfaces(prepareExtraInterfaces(source));
return settings;
}
+ private static CreationSettings validatedStaticSettings(
+ Class classToMock, CreationSettings source) {
+
+ if (classToMock.isPrimitive()) {
+ throw new MockitoException("Cannot create static mock of primitive type " + classToMock);
+ }
+ if (!source.getExtraInterfaces().isEmpty()) {
+ throw new MockitoException("Cannot specify additional interfaces for static mock of " + classToMock);
+ }
+ if (source.getSpiedInstance() != null) {
+ throw new MockitoException("Cannot specify spied instance for static mock of " + classToMock);
+ }
+
+ CreationSettings settings = new CreationSettings(source);
+ settings.setMockName(new MockNameImpl(source.getName(), classToMock, true));
+ settings.setTypeToMock(classToMock);
+ return settings;
+ }
+
private static Set> prepareExtraInterfaces(CreationSettings settings) {
Set> interfaces = new HashSet>(settings.getExtraInterfaces());
if (settings.isSerializable()) {
diff --git a/src/main/java/org/mockito/internal/creation/StaticMockControl.java b/src/main/java/org/mockito/internal/creation/StaticMockControl.java
new file mode 100644
index 0000000000..7bc90f3ee4
--- /dev/null
+++ b/src/main/java/org/mockito/internal/creation/StaticMockControl.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2007 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.internal.creation;
+
+import org.mockito.Incubating;
+
+@Incubating
+public interface StaticMockControl {
+
+ Class getType();
+
+ void enable();
+
+ void disable();
+}
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java
index 3b124dcaab..f2fa215804 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyMockMaker.java
@@ -5,6 +5,7 @@
package org.mockito.internal.creation.bytebuddy;
import org.mockito.Incubating;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
@@ -45,4 +46,9 @@ public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings
public TypeMockability isTypeMockable(Class> type) {
return defaultByteBuddyMockMaker.isTypeMockable(type);
}
+
+ @Override
+ public StaticMockControl createStaticMock(Class type, MockCreationSettings settings, MockHandler handler) {
+ return defaultByteBuddyMockMaker.createStaticMock(type, settings, handler);
+ }
}
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java
index a131c04bc7..b87d7cb6b7 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/BytecodeGenerator.java
@@ -7,4 +7,6 @@
public interface BytecodeGenerator {
Class extends T> mockClass(MockFeatures features);
+
+ void mockClassStatic(Class> type);
}
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java
index d110451e3d..66bf1dfbe7 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineByteBuddyMockMaker.java
@@ -13,6 +13,9 @@
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Modifier;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@@ -25,7 +28,9 @@
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.base.MockitoInitializationException;
import org.mockito.internal.configuration.plugins.Plugins;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.internal.util.Platform;
+import org.mockito.internal.util.concurrent.DetachedThreadLocal;
import org.mockito.internal.util.concurrent.WeakConcurrentMap;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
@@ -182,6 +187,9 @@ public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineM
private final WeakConcurrentMap mocks =
new WeakConcurrentMap.WithInlinedExpunction();
+ private final DetachedThreadLocal, MockMethodInterceptor>> mockedStatics =
+ new DetachedThreadLocal<>(DetachedThreadLocal.Cleaner.INLINE);
+
public InlineByteBuddyMockMaker() {
if (INITIALIZATION_ERROR != null) {
throw new MockitoInitializationException(
@@ -195,7 +203,7 @@ public InlineByteBuddyMockMaker() {
}
bytecodeGenerator =
new TypeCachingBytecodeGenerator(
- new InlineBytecodeGenerator(INSTRUMENTATION, mocks), true);
+ new InlineBytecodeGenerator(INSTRUMENTATION, mocks, mockedStatics), true);
}
@Override
@@ -288,7 +296,13 @@ private RuntimeException prettifyFailure(
@Override
public MockHandler getHandler(Object mock) {
- MockMethodInterceptor interceptor = mocks.get(mock);
+ MockMethodInterceptor interceptor;
+ if (mock instanceof Class>) {
+ Map, MockMethodInterceptor> interceptors = mockedStatics.get();
+ interceptor = interceptors != null ? interceptors.get(mock) : null;
+ } else {
+ interceptor = mocks.get(mock);
+ }
if (interceptor == null) {
return null;
} else {
@@ -298,21 +312,38 @@ public MockHandler getHandler(Object mock) {
@Override
public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
- MockMethodInterceptor mockMethodInterceptor =
- new MockMethodInterceptor(newHandler, settings);
- mocks.put(mock, mockMethodInterceptor);
- if (mock instanceof MockAccess) {
- ((MockAccess) mock).setMockitoInterceptor(mockMethodInterceptor);
+ MockMethodInterceptor mockMethodInterceptor = new MockMethodInterceptor(newHandler, settings);
+ if (mock instanceof Class>) {
+ Map, MockMethodInterceptor> interceptors = mockedStatics.get();
+ if (interceptors == null || !interceptors.containsKey(mock)) {
+ throw new MockitoException("Cannot reset " + mock + " which is not currently registered as a static mock");
+ }
+ interceptors.put((Class>) mock, mockMethodInterceptor);
+ } else {
+ if (!mocks.containsKey(mock)) {
+ throw new MockitoException("Cannot reset " + mock + " which is not currently registered as a mock");
+ }
+ mocks.put(mock, mockMethodInterceptor);
+ if (mock instanceof MockAccess) {
+ ((MockAccess) mock).setMockitoInterceptor(mockMethodInterceptor);
+ }
}
}
@Override
public void clearMock(Object mock) {
- mocks.remove(mock);
+ if (mock instanceof Class>) {
+ for (Map, ?> entry : mockedStatics.getBackingMap().target.values()) {
+ entry.remove(mock);
+ }
+ } else {
+ mocks.remove(mock);
+ }
}
@Override
public void clearAllMocks() {
+ mockedStatics.getBackingMap().clear();
mocks.clear();
}
@@ -339,4 +370,71 @@ public String nonMockableReason() {
}
};
}
+
+ @Override
+ public StaticMockControl createStaticMock(Class type, MockCreationSettings settings, MockHandler handler) {
+ if (type == ConcurrentHashMap.class) {
+ throw new MockitoException("It is not possible to mock static methods of ConcurrentHashMap "
+ + "to avoid infinitive loops within Mockito's implementation of static mock handling");
+ }
+
+ bytecodeGenerator.mockClassStatic(type);
+
+ Map, MockMethodInterceptor> interceptors = mockedStatics.get();
+ if (interceptors == null) {
+ interceptors = new WeakHashMap<>();
+ mockedStatics.set(interceptors);
+ }
+
+ return new InlineStaticMockControl<>(type, interceptors, settings, handler);
+ }
+
+ private static class InlineStaticMockControl implements StaticMockControl {
+
+ private final Class type;
+
+ private final Map, MockMethodInterceptor> interceptors;
+
+ private final MockCreationSettings settings;
+ private final MockHandler handler;
+
+ private InlineStaticMockControl(
+ Class type,
+ Map, MockMethodInterceptor> interceptors,
+ MockCreationSettings settings,
+ MockHandler handler
+ ) {
+ this.type = type;
+ this.interceptors = interceptors;
+ this.settings = settings;
+ this.handler = handler;
+ }
+
+ @Override
+ public Class getType() {
+ return type;
+ }
+
+ @Override
+ public void enable() {
+ if (interceptors.putIfAbsent(type, new MockMethodInterceptor(handler, settings)) != null) {
+ throw new MockitoException(join(
+ "For " + type.getName() + ", static mocking is already registered in the current thread",
+ "",
+ "To create a new mock, the existing static mock registration must be deregistered"
+ ));
+ }
+ }
+
+ @Override
+ public void disable() {
+ if (interceptors.remove(type) == null) {
+ throw new MockitoException(join(
+ "Could not deregister " + type.getName() + " as a static mock since it is not currently registered",
+ "",
+ "To register a static mock, use Mockito.mockStatic(" + type.getSimpleName() + ".class)"
+ ));
+ }
+ }
+ }
}
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java
index dfca195b4d..bdd93f9986 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineBytecodeGenerator.java
@@ -38,6 +38,7 @@
import net.bytebuddy.utility.RandomString;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher;
+import org.mockito.internal.util.concurrent.DetachedThreadLocal;
import org.mockito.internal.util.concurrent.WeakConcurrentMap;
import org.mockito.internal.util.concurrent.WeakConcurrentSet;
import org.mockito.mock.SerializableMode;
@@ -63,7 +64,7 @@ public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTran
private final Instrumentation instrumentation;
private final ByteBuddy byteBuddy;
- private final WeakConcurrentSet> mocked;
+ private final WeakConcurrentSet> mocked, flatMocked;
private final BytecodeGenerator subclassEngine;
private final AsmVisitorWrapper mockTransformer;
@@ -73,7 +74,8 @@ public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTran
public InlineBytecodeGenerator(
Instrumentation instrumentation,
- WeakConcurrentMap mocks) {
+ WeakConcurrentMap mocks,
+ DetachedThreadLocal, MockMethodInterceptor>> mockedStatics) {
preload();
this.instrumentation = instrumentation;
byteBuddy =
@@ -81,7 +83,8 @@ public InlineBytecodeGenerator(
.with(TypeValidation.DISABLED)
.with(Implementation.Context.Disabled.Factory.INSTANCE)
.with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE);
- mocked = new WeakConcurrentSet>(WeakConcurrentSet.Cleaner.INLINE);
+ mocked = new WeakConcurrentSet<>(WeakConcurrentSet.Cleaner.INLINE);
+ flatMocked = new WeakConcurrentSet<>(WeakConcurrentSet.Cleaner.INLINE);
String identifier = RandomString.make();
subclassEngine =
new TypeCachingBytecodeGenerator(
@@ -110,6 +113,11 @@ public InlineBytecodeGenerator(
Advice.withCustomMapping()
.bind(MockMethodAdvice.Identifier.class, identifier)
.to(MockMethodAdvice.class))
+ .method(
+ isStatic(),
+ Advice.withCustomMapping()
+ .bind(MockMethodAdvice.Identifier.class, identifier)
+ .to(MockMethodAdvice.ForStatic.class))
.method(
isHashCode(),
Advice.withCustomMapping()
@@ -141,7 +149,7 @@ public InlineBytecodeGenerator(
this.getModule = getModule;
this.canRead = canRead;
this.redefineModule = redefineModule;
- MockMethodDispatcher.set(identifier, new MockMethodAdvice(mocks, identifier));
+ MockMethodDispatcher.set(identifier, new MockMethodAdvice(mocks, mockedStatics, identifier));
instrumentation.addTransformer(this, true);
}
@@ -182,27 +190,46 @@ public Class extends T> mockClass(MockFeatures features) {
checkSupportedCombination(subclassingRequired, features);
+ Set> types = new HashSet<>();
+ types.add(features.mockedType);
+ types.addAll(features.interfaces);
synchronized (this) {
- triggerRetransformation(features);
+ triggerRetransformation(types, false);
}
return subclassingRequired ? subclassEngine.mockClass(features) : features.mockedType;
}
- private void triggerRetransformation(MockFeatures features) {
- Set> types = new HashSet>();
- Class> type = features.mockedType;
- do {
- if (mocked.add(type)) {
- types.add(type);
- addInterfaces(types, type.getInterfaces());
+ @Override
+ public void mockClassStatic(Class> type) {
+ triggerRetransformation(Collections.singleton(type), true);
+ }
+
+ private void triggerRetransformation(Set> types, boolean flat) {
+ Set> targets = new HashSet>();
+
+ for (Class> type : types) {
+ if (flat) {
+ if (!mocked.contains(type) && flatMocked.add(type)) {
+ targets.add(type);
+ }
+ } else {
+ do {
+ if (mocked.add(type)) {
+ if (!flatMocked.remove(type)) {
+ targets.add(type);
+ }
+ addInterfaces(targets, type.getInterfaces());
+ }
+ type = type.getSuperclass();
+ } while (type != null);
}
- type = type.getSuperclass();
- } while (type != null);
- if (!types.isEmpty()) {
+ }
+
+ if (!targets.isEmpty()) {
try {
- assureCanReadMockito(types);
- instrumentation.retransformClasses(types.toArray(new Class>[types.size()]));
+ assureCanReadMockito(targets);
+ instrumentation.retransformClasses(targets.toArray(new Class>[targets.size()]));
Throwable throwable = lastException;
if (throwable != null) {
throw new IllegalStateException(
@@ -215,10 +242,11 @@ private void triggerRetransformation(MockFeatures features) {
throwable);
}
} catch (Exception exception) {
- for (Class> failed : types) {
+ for (Class> failed : targets) {
mocked.remove(failed);
+ flatMocked.remove(failed);
}
- throw new MockitoException("Could not modify all classes " + types, exception);
+ throw new MockitoException("Could not modify all classes " + targets, exception);
} finally {
lastException = null;
}
@@ -281,7 +309,9 @@ private void checkSupportedCombination(
private void addInterfaces(Set> types, Class>[] interfaces) {
for (Class> type : interfaces) {
if (mocked.add(type)) {
- types.add(type);
+ if (!flatMocked.remove(type)) {
+ types.add(type);
+ }
addInterfaces(types, type.getInterfaces());
}
}
@@ -296,6 +326,7 @@ public byte[] transform(
byte[] classfileBuffer) {
if (classBeingRedefined == null
|| !mocked.contains(classBeingRedefined)
+ && !flatMocked.contains(classBeingRedefined)
|| EXCLUDES.contains(classBeingRedefined)) {
return null;
} else {
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java
index 87640a4441..ef74204fa6 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java
@@ -13,6 +13,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.util.Map;
import java.util.concurrent.Callable;
import net.bytebuddy.asm.Advice;
@@ -30,11 +31,13 @@
import org.mockito.internal.invocation.SerializableMethod;
import org.mockito.internal.invocation.mockref.MockReference;
import org.mockito.internal.invocation.mockref.MockWeakReference;
+import org.mockito.internal.util.concurrent.DetachedThreadLocal;
import org.mockito.internal.util.concurrent.WeakConcurrentMap;
public class MockMethodAdvice extends MockMethodDispatcher {
private final WeakConcurrentMap interceptors;
+ private final DetachedThreadLocal, MockMethodInterceptor>> mockedStatics;
private final String identifier;
@@ -44,8 +47,11 @@ public class MockMethodAdvice extends MockMethodDispatcher {
new WeakConcurrentMap.WithInlinedExpunction, SoftReference>();
public MockMethodAdvice(
- WeakConcurrentMap interceptors, String identifier) {
+ WeakConcurrentMap interceptors,
+ DetachedThreadLocal, MockMethodInterceptor>> mockedStatics,
+ String identifier) {
this.interceptors = interceptors;
+ this.mockedStatics = mockedStatics;
this.identifier = identifier;
}
@@ -120,6 +126,21 @@ public Callable> handle(Object instance, Method origin, Object[] arguments) th
new LocationImpl(new Throwable(), true)));
}
+ @Override
+ public Callable> handleStatic(Class> type, Method origin, Object[] arguments) throws Throwable {
+ Map, MockMethodInterceptor> interceptors = mockedStatics.get();
+ if (interceptors == null || !interceptors.containsKey(type)) {
+ return null;
+ }
+ return new ReturnValueWrapper(
+ interceptors.get(type).doIntercept(
+ type,
+ origin,
+ arguments,
+ new StaticMethodCall(selfCallInfo, type, origin, arguments),
+ new LocationImpl(new Throwable(), true)));
+ }
+
@Override
public boolean isMock(Object instance) {
// We need to exclude 'interceptors.target' explicitly to avoid a recursive check on whether
@@ -132,6 +153,12 @@ public boolean isMocked(Object instance) {
return selfCallInfo.checkSuperCall(instance) && isMock(instance);
}
+ @Override
+ public boolean isMockedStatic(Class> type) {
+ Map, ?> interceptors = mockedStatics.get();
+ return interceptors != null && interceptors.containsKey(type);
+ }
+
@Override
public boolean isOverridden(Object instance, Method origin) {
SoftReference reference = graphs.get(instance.getClass());
@@ -230,6 +257,39 @@ public Object invoke() throws Throwable {
}
}
+ private static class StaticMethodCall implements RealMethod {
+
+ private final SelfCallInfo selfCallInfo;
+
+ private final Class> type;
+
+ private final Method origin;
+
+ private final Object[] arguments;
+
+ private StaticMethodCall(SelfCallInfo selfCallInfo, Class> type, Method origin, Object[] arguments) {
+ this.selfCallInfo = selfCallInfo;
+ this.type = type;
+ this.origin = origin;
+ this.arguments = arguments;
+ }
+
+ @Override
+ public boolean isInvokable() {
+ return true;
+ }
+
+ @Override
+ public Object invoke() throws Throwable {
+ if (!Modifier.isPublic(
+ type.getModifiers() & origin.getModifiers())) {
+ origin.setAccessible(true);
+ }
+ selfCallInfo.set(type);
+ return tryInvoke(origin, null, arguments);
+ }
+ }
+
private static Object tryInvoke(Method origin, Object instance, Object[] arguments)
throws Throwable {
try {
@@ -324,6 +384,36 @@ private static void enter(
}
}
+ static class ForStatic {
+
+ @SuppressWarnings("unused")
+ @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
+ private static Callable> enter(
+ @Identifier String identifier,
+ @Advice.Origin Class> type,
+ @Advice.Origin Method origin,
+ @Advice.AllArguments Object[] arguments)
+ throws Throwable {
+ MockMethodDispatcher dispatcher = MockMethodDispatcher.getStatic(identifier, type);
+ if (dispatcher == null || !dispatcher.isMockedStatic(type)) {
+ return null;
+ } else {
+ return dispatcher.handleStatic(type, origin, arguments);
+ }
+ }
+
+ @SuppressWarnings({"unused", "UnusedAssignment"})
+ @Advice.OnMethodExit
+ private static void exit(
+ @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object returned,
+ @Advice.Enter Callable> mocked)
+ throws Throwable {
+ if (mocked != null) {
+ returned = mocked.call();
+ }
+ }
+ }
+
public static class ForReadObject {
@SuppressWarnings("unused")
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java
index e112d9e281..926c8d20c1 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassByteBuddyMockMaker.java
@@ -11,6 +11,7 @@
import org.mockito.creation.instance.Instantiator;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.configuration.plugins.Plugins;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.internal.util.Platform;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
@@ -179,4 +180,14 @@ public String nonMockableReason() {
}
};
}
+
+ @Override
+ public StaticMockControl createStaticMock(Class type, MockCreationSettings settings, MockHandler handler) {
+ throw new MockitoException(join(
+ "The regular, subclass-based MockMaker does not support the creation of static mocks",
+ "",
+ "To enable static mocks, you need to use Mockito's inline mock maker which is based on the Instrumentation API.",
+ "You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'."
+ ));
+ }
}
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java
index f323f23d44..4a65643bc5 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java
@@ -203,6 +203,11 @@ public Class extends T> mockClass(MockFeatures features) {
.getLoaded();
}
+ @Override
+ public void mockClassStatic(Class> type) {
+ throw new MockitoException("The subclass byte code generator cannot create static mocks");
+ }
+
private Collection> getAllTypes(Class type) {
Collection> supertypes = new LinkedList>();
supertypes.add(type);
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java
index 18ca2423a6..76bf44dca8 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/TypeCachingBytecodeGenerator.java
@@ -57,6 +57,11 @@ public Class> call() throws Exception {
}
}
+ @Override
+ public void mockClassStatic(Class> type) {
+ bytecodeGenerator.mockClassStatic(type);
+ }
+
private static class MockitoMockKey extends TypeCache.SimpleKey {
private final SerializableMode serializableMode;
diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java b/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java
index 6c077cfd9e..8055d5f134 100644
--- a/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java
+++ b/src/main/java/org/mockito/internal/creation/bytebuddy/inject/MockMethodDispatcher.java
@@ -11,29 +11,41 @@
public abstract class MockMethodDispatcher {
- private static final ConcurrentMap INSTANCE =
- new ConcurrentHashMap();
+ private static final ConcurrentMap DISPATCHERS = new ConcurrentHashMap<>();
public static MockMethodDispatcher get(String identifier, Object mock) {
- if (mock
- == INSTANCE) { // Avoid endless loop if ConcurrentHashMap was redefined to check for
- // being a mock.
+ if (mock == DISPATCHERS) {
+ // Avoid endless loop if ConcurrentHashMap was redefined to check for being a mock.
return null;
} else {
- return INSTANCE.get(identifier);
+ return DISPATCHERS.get(identifier);
+ }
+ }
+
+ public static MockMethodDispatcher getStatic(String identifier, Class> type) {
+ if (MockMethodDispatcher.class.isAssignableFrom(type) || type == ConcurrentHashMap.class) {
+ // Avoid endless loop for lookups of self.
+ return null;
+ } else {
+ return DISPATCHERS.get(identifier);
}
}
public static void set(String identifier, MockMethodDispatcher dispatcher) {
- INSTANCE.putIfAbsent(identifier, dispatcher);
+ DISPATCHERS.putIfAbsent(identifier, dispatcher);
}
public abstract Callable> handle(Object instance, Method origin, Object[] arguments)
throws Throwable;
+ public abstract Callable> handleStatic(Class> type, Method origin, Object[] arguments)
+ throws Throwable;
+
public abstract boolean isMock(Object instance);
public abstract boolean isMocked(Object instance);
+ public abstract boolean isMockedStatic(Class> type);
+
public abstract boolean isOverridden(Object instance, Method origin);
}
diff --git a/src/main/java/org/mockito/internal/framework/DefaultMockitoSession.java b/src/main/java/org/mockito/internal/framework/DefaultMockitoSession.java
index 5e60099726..2fa7a1767c 100644
--- a/src/main/java/org/mockito/internal/framework/DefaultMockitoSession.java
+++ b/src/main/java/org/mockito/internal/framework/DefaultMockitoSession.java
@@ -4,11 +4,10 @@
*/
package org.mockito.internal.framework;
-import java.util.List;
-
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
+import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.misusing.RedundantListenerException;
import org.mockito.internal.exceptions.Reporter;
import org.mockito.internal.junit.TestFinishedEvent;
@@ -16,11 +15,16 @@
import org.mockito.plugins.MockitoLogger;
import org.mockito.quality.Strictness;
+import java.util.ArrayList;
+import java.util.List;
+
public class DefaultMockitoSession implements MockitoSession {
private final String name;
private final UniversalTestListener listener;
+ private final List closeables = new ArrayList<>();
+
public DefaultMockitoSession(
List testClassInstances,
String name,
@@ -36,9 +40,11 @@ public DefaultMockitoSession(
}
try {
for (Object testClassInstance : testClassInstances) {
- MockitoAnnotations.initMocks(testClassInstance);
+ closeables.add(MockitoAnnotations.initMocks(testClassInstance));
}
} catch (RuntimeException e) {
+ release();
+
// clean up in case 'initMocks' fails
listener.setListenerDirty();
throw e;
@@ -52,11 +58,15 @@ public void setStrictness(Strictness strictness) {
@Override
public void finishMocking() {
+ release();
+
finishMocking(null);
}
@Override
public void finishMocking(final Throwable failure) {
+ release();
+
// Cleaning up the state, we no longer need the listener hooked up
// The listener implements MockCreationListener and at this point
// we no longer need to listen on mock creation events. We are wrapping up the session
@@ -82,4 +92,14 @@ public String getTestName() {
Mockito.validateMockitoUsage();
}
}
+
+ private void release() {
+ for (AutoCloseable closeable : closeables) {
+ try {
+ closeable.close();
+ } catch (Exception e) {
+ throw new MockitoException("Failed to release mocks", e);
+ }
+ }
+ }
}
diff --git a/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java b/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java
index 4ef871dce5..57bb4b71e7 100644
--- a/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java
+++ b/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java
@@ -123,18 +123,6 @@ public T getArgument(int index) {
return (T) arguments[index];
}
- public MockReference getMockRef() {
- return mockRef;
- }
-
- public MockitoMethod getMockitoMethod() {
- return mockitoMethod;
- }
-
- public RealMethod getRealMethod() {
- return realMethod;
- }
-
@Override
public List getArgumentsAsMatchers() {
return argumentsToMatchers(getArguments());
diff --git a/src/main/java/org/mockito/internal/junit/JUnitSessionStore.java b/src/main/java/org/mockito/internal/junit/JUnitSessionStore.java
index fa065acd9c..5b5951960a 100644
--- a/src/main/java/org/mockito/internal/junit/JUnitSessionStore.java
+++ b/src/main/java/org/mockito/internal/junit/JUnitSessionStore.java
@@ -26,6 +26,7 @@ class JUnitSessionStore {
Statement createStatement(final Statement base, final String methodName, final Object target) {
return new Statement() {
public void evaluate() throws Throwable {
+ AutoCloseable closeable;
if (session == null) {
session =
Mockito.mockitoSession()
@@ -34,11 +35,15 @@ public void evaluate() throws Throwable {
.logger(new MockitoSessionLoggerAdapter(logger))
.initMocks(target)
.startMocking();
+ closeable = null;
} else {
- MockitoAnnotations.initMocks(target);
+ closeable = MockitoAnnotations.initMocks(target);
}
Throwable testFailure = evaluateSafely(base);
session.finishMocking(testFailure);
+ if (closeable != null) {
+ closeable.close();
+ }
if (testFailure != null) {
throw testFailure;
}
diff --git a/src/main/java/org/mockito/internal/runners/DefaultInternalRunner.java b/src/main/java/org/mockito/internal/runners/DefaultInternalRunner.java
index f81b01560b..7fbbd07df4 100644
--- a/src/main/java/org/mockito/internal/runners/DefaultInternalRunner.java
+++ b/src/main/java/org/mockito/internal/runners/DefaultInternalRunner.java
@@ -40,14 +40,23 @@ protected Statement withBefores(
return new Statement() {
@Override
public void evaluate() throws Throwable {
+ AutoCloseable closeable;
if (mockitoTestListener == null) {
// get new test listener and add it to the framework
mockitoTestListener = listenerSupplier.get();
Mockito.framework().addListener(mockitoTestListener);
// init annotated mocks before tests
- MockitoAnnotations.initMocks(target);
+ closeable = MockitoAnnotations.initMocks(target);
+ } else {
+ closeable = null;
+ }
+ try {
+ base.evaluate();
+ } finally {
+ if (closeable != null) {
+ closeable.close();
+ }
}
- base.evaluate();
}
};
}
diff --git a/src/main/java/org/mockito/internal/util/MockNameImpl.java b/src/main/java/org/mockito/internal/util/MockNameImpl.java
index a06975871d..ef0307fc11 100644
--- a/src/main/java/org/mockito/internal/util/MockNameImpl.java
+++ b/src/main/java/org/mockito/internal/util/MockNameImpl.java
@@ -15,9 +15,9 @@ public class MockNameImpl implements MockName, Serializable {
private boolean defaultName;
@SuppressWarnings("unchecked")
- public MockNameImpl(String mockName, Class> classToMock) {
+ public MockNameImpl(String mockName, Class> type, boolean mockedStatic) {
if (mockName == null) {
- this.mockName = toInstanceName(classToMock);
+ this.mockName = mockedStatic ? toClassName(type) : toInstanceName(type);
this.defaultName = true;
} else {
this.mockName = mockName;
@@ -38,6 +38,15 @@ private static String toInstanceName(Class> clazz) {
return className.substring(0, 1).toLowerCase() + className.substring(1);
}
+ private static String toClassName(Class> clazz) {
+ String className = clazz.getSimpleName();
+ if (className.length() == 0) {
+ // it's an anonymous class, let's get name from the parent
+ className = clazz.getSuperclass().getSimpleName() + "$";
+ }
+ return className + ".class";
+ }
+
public boolean isDefault() {
return defaultName;
}
diff --git a/src/main/java/org/mockito/internal/util/MockUtil.java b/src/main/java/org/mockito/internal/util/MockUtil.java
index 7dec584f4e..276a1d5ffc 100644
--- a/src/main/java/org/mockito/internal/util/MockUtil.java
+++ b/src/main/java/org/mockito/internal/util/MockUtil.java
@@ -9,6 +9,7 @@
import org.mockito.Mockito;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.internal.configuration.plugins.Plugins;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.internal.creation.settings.CreationSettings;
import org.mockito.internal.stubbing.InvocationContainerImpl;
import org.mockito.internal.util.reflection.LenientCopyTool;
@@ -102,4 +103,9 @@ public static void maybeRedefineMockName(Object mock, String newName) {
public static MockCreationSettings getMockSettings(Object mock) {
return getMockHandler(mock).getMockSettings();
}
+
+ public static StaticMockControl createStaticMock(Class type, MockCreationSettings settings) {
+ MockHandler handler = createMockHandler(settings);
+ return mockMaker.createStaticMock(type, settings, handler);
+ }
}
diff --git a/src/main/java/org/mockito/internal/util/concurrent/WeakConcurrentSet.java b/src/main/java/org/mockito/internal/util/concurrent/WeakConcurrentSet.java
index e9bc1cea2d..f3417c020e 100644
--- a/src/main/java/org/mockito/internal/util/concurrent/WeakConcurrentSet.java
+++ b/src/main/java/org/mockito/internal/util/concurrent/WeakConcurrentSet.java
@@ -53,7 +53,7 @@ public boolean contains(V value) {
* @return {@code true} if the value is contained in the set.
*/
public boolean remove(V value) {
- return target.remove(value);
+ return target.remove(value) != null;
}
/**
diff --git a/src/main/java/org/mockito/plugins/AnnotationEngine.java b/src/main/java/org/mockito/plugins/AnnotationEngine.java
index c60f477b9f..16741822a6 100644
--- a/src/main/java/org/mockito/plugins/AnnotationEngine.java
+++ b/src/main/java/org/mockito/plugins/AnnotationEngine.java
@@ -25,5 +25,11 @@ public interface AnnotationEngine {
* @param clazz Class where to extract field information, check implementation for details
* @param testInstance Test instance
*/
- void process(Class> clazz, Object testInstance);
+ AutoCloseable process(Class> clazz, Object testInstance);
+
+ class NoAction implements AutoCloseable {
+
+ @Override
+ public void close() { }
+ }
}
diff --git a/src/main/java/org/mockito/plugins/MockMaker.java b/src/main/java/org/mockito/plugins/MockMaker.java
index f2798ed8bf..201eabc387 100644
--- a/src/main/java/org/mockito/plugins/MockMaker.java
+++ b/src/main/java/org/mockito/plugins/MockMaker.java
@@ -5,6 +5,7 @@
package org.mockito.plugins;
import org.mockito.Incubating;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.invocation.MockHandler;
import org.mockito.mock.MockCreationSettings;
@@ -108,6 +109,25 @@ public interface MockMaker {
@Incubating
TypeMockability isTypeMockable(Class> type);
+ /**
+ * If you want to provide your own implementation of {@code MockMaker} this method should:
+ *
+ * Alter the supplied class to only change its behavior in the current thread.
+ * Only alters the static method's behavior after being enabled.
+ * Stops the altered behavior when disabled.
+ *
+ *
+ * @param settings Mock creation settings like type to mock, extra interfaces and so on.
+ * @param handler See {@link org.mockito.invocation.MockHandler}.
+ * Do not provide your own implementation at this time. Make sure your implementation of
+ * {@link #getHandler(Object)} will return this instance.
+ * @param Type of the mock to return, actually the settings.getTypeToMock
.
+ * @return A control for the static mock.
+ * @since 3.4.0
+ */
+ @Incubating
+ StaticMockControl createStaticMock(Class type, MockCreationSettings settings, MockHandler handler);
+
/**
* Carries the mockability information
*
diff --git a/src/test/java/org/mockito/MockitoTest.java b/src/test/java/org/mockito/MockitoTest.java
index 008bf7f579..23df60e08e 100644
--- a/src/test/java/org/mockito/MockitoTest.java
+++ b/src/test/java/org/mockito/MockitoTest.java
@@ -11,6 +11,7 @@
import java.util.List;
import org.junit.Test;
+import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.exceptions.misusing.NullInsteadOfMockException;
import org.mockito.internal.creation.MockSettingsImpl;
@@ -64,6 +65,12 @@ public void shouldValidateMockWhenCreatingInOrderObject() {
Mockito.inOrder("notMock");
}
+ @SuppressWarnings({"CheckReturnValue", "MockitoUsage"})
+ @Test(expected = MockitoException.class)
+ public void shouldGiveExplantionOnStaticMockingWithoutInlineMockMaker() {
+ Mockito.mockStatic(Object.class);
+ }
+
@Test
public void shouldStartingMockSettingsContainDefaultBehavior() {
// when
diff --git a/src/test/java/org/mockito/internal/configuration/GlobalConfigurationTest.java b/src/test/java/org/mockito/internal/configuration/GlobalConfigurationTest.java
index 79d4fedd21..d5590ccfa6 100644
--- a/src/test/java/org/mockito/internal/configuration/GlobalConfigurationTest.java
+++ b/src/test/java/org/mockito/internal/configuration/GlobalConfigurationTest.java
@@ -59,6 +59,8 @@ public void reset_annotation_engine() {
private static class CustomAnnotationEngine implements AnnotationEngine {
@Override
- public void process(Class> clazz, Object testInstance) {}
+ public AutoCloseable process(Class> clazz, Object testInstance) {
+ return new NoAction();
+ }
}
}
diff --git a/src/test/java/org/mockito/internal/util/MockNameImplTest.java b/src/test/java/org/mockito/internal/util/MockNameImplTest.java
index e0bc019dc4..583bd7ac06 100644
--- a/src/test/java/org/mockito/internal/util/MockNameImplTest.java
+++ b/src/test/java/org/mockito/internal/util/MockNameImplTest.java
@@ -14,25 +14,51 @@ public class MockNameImplTest extends TestBase {
@Test
public void shouldProvideTheNameForClass() throws Exception {
// when
- String name = new MockNameImpl(null, SomeClass.class).toString();
+ String name = new MockNameImpl(null, SomeClass.class, false).toString();
// then
assertEquals("someClass", name);
}
+ @Test
+ public void shouldProvideTheNameForClassOnStaticMock() throws Exception {
+ // when
+ String name = new MockNameImpl(null, SomeClass.class, true).toString();
+ // then
+ assertEquals("SomeClass.class", name);
+ }
+
@Test
public void shouldProvideTheNameForAnonymousClass() throws Exception {
// given
SomeInterface anonymousInstance = new SomeInterface() {};
// when
- String name = new MockNameImpl(null, anonymousInstance.getClass()).toString();
+ String name = new MockNameImpl(null, anonymousInstance.getClass(), false).toString();
// then
assertEquals("someInterface", name);
}
+ @Test
+ public void shouldProvideTheNameForAnonymousClassOnStatic() throws Exception {
+ // given
+ SomeInterface anonymousInstance = new SomeInterface() {};
+ // when
+ String name = new MockNameImpl(null, anonymousInstance.getClass(), true).toString();
+ // then
+ assertEquals("SomeInterface$.class", name);
+ }
+
@Test
public void shouldProvideTheGivenName() throws Exception {
// when
- String name = new MockNameImpl("The Hulk", SomeClass.class).toString();
+ String name = new MockNameImpl("The Hulk", SomeClass.class, false).toString();
+ // then
+ assertEquals("The Hulk", name);
+ }
+
+ @Test
+ public void shouldProvideTheGivenNameOnStatic() throws Exception {
+ // when
+ String name = new MockNameImpl("The Hulk", SomeClass.class, true).toString();
// then
assertEquals("The Hulk", name);
}
diff --git a/subprojects/android/src/main/java/org/mockito/android/internal/creation/AndroidByteBuddyMockMaker.java b/subprojects/android/src/main/java/org/mockito/android/internal/creation/AndroidByteBuddyMockMaker.java
index db77fb7b03..013c283893 100644
--- a/subprojects/android/src/main/java/org/mockito/android/internal/creation/AndroidByteBuddyMockMaker.java
+++ b/subprojects/android/src/main/java/org/mockito/android/internal/creation/AndroidByteBuddyMockMaker.java
@@ -4,7 +4,9 @@
*/
package org.mockito.android.internal.creation;
+import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.configuration.plugins.Plugins;
+import org.mockito.internal.creation.StaticMockControl;
import org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker;
import org.mockito.internal.util.Platform;
import org.mockito.invocation.MockHandler;
@@ -39,6 +41,15 @@ public T createMock(MockCreationSettings settings, MockHandler handler) {
return delegate.createMock(settings, handler);
}
+ @Override
+ public StaticMockControl createStaticMock(Class type, MockCreationSettings settings, MockHandler handler) {
+ throw new MockitoException(join(
+ "The Android mock maker does not support static mocks.",
+ "",
+ "Support for static mocks is based on the Instrumentation API which Android does not support."
+ ));
+ }
+
@Override
public MockHandler getHandler(Object mock) {
return delegate.getHandler(mock);
diff --git a/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoAnyClassWithPrimitiveType.java b/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoAnyClassWithPrimitiveType.java
index a076f2a0e7..f99c7eeb1b 100644
--- a/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoAnyClassWithPrimitiveType.java
+++ b/subprojects/errorprone/src/main/java/org/mockito/errorprone/bugpatterns/MockitoAnyClassWithPrimitiveType.java
@@ -12,7 +12,7 @@
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
-import com.google.errorprone.matchers.method.MethodMatchers.MethodNameMatcher;
+import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Type;
@@ -42,7 +42,7 @@ public class MockitoAnyClassWithPrimitiveType extends AbstractMockitoAnyForPrimi
};
// Match against the any() or any(Class) methods.
- private static final MethodNameMatcher GENERIC_ANY =
+ private static final Matcher GENERIC_ANY =
Matchers.staticMethod().onClassAny(CLASS_NAMES).named("any");
@Override
diff --git a/subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java b/subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java
new file mode 100644
index 0000000000..9a61e256e1
--- /dev/null
+++ b/subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2020 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockitoinline;
+
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.exceptions.base.MockitoException;
+import org.mockito.exceptions.verification.NoInteractionsWanted;
+import org.mockito.exceptions.verification.WantedButNotInvoked;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import static junit.framework.TestCase.*;
+import static org.mockito.Mockito.times;
+
+public final class StaticMockTest {
+
+ @Test
+ public void testStaticMockSimple() {
+ assertEquals("foo", Dummy.foo());
+ try (MockedStatic ignored = Mockito.mockStatic(Dummy.class)) {
+ assertNull(Dummy.foo());
+ }
+ assertEquals("foo", Dummy.foo());
+ }
+
+ @Test
+ public void testStaticMockWithVerification() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(Dummy::foo);
+ }
+ }
+
+ @Test(expected = WantedButNotInvoked.class)
+ public void testStaticMockWithVerificationFailed() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.verify(Dummy::foo);
+ }
+ }
+
+ @Test
+ public void testStaticMockWithMoInteractions() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ dummy.verifyNoInteractions();
+ }
+ }
+
+ @Test(expected = NoInteractionsWanted.class)
+ public void testStaticMockWithMoInteractionsFailed() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ public void testStaticMockWithMoMoreInteractions() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(Dummy::foo);
+ dummy.verifyNoMoreInteractions();
+ }
+ }
+
+ @Test(expected = NoInteractionsWanted.class)
+ public void testStaticMockWithMoMoreInteractionsFailed() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verifyNoMoreInteractions();
+ }
+ }
+
+ @Test
+ public void testStaticMockWithDefaultAnswer() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class, invocation -> "bar")) {
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(Dummy::foo);
+ }
+ }
+
+ @Test
+ public void testStaticMockReset() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ dummy.reset();
+ assertNull(Dummy.foo());
+ }
+ }
+
+ @Test
+ public void testStaticMockClear() {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.clearInvocations();
+ dummy.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ public void testStaticMockDoesNotAffectDifferentThread() throws InterruptedException {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(Dummy::foo);
+ AtomicReference reference = new AtomicReference<>();
+ Thread thread = new Thread(() -> reference.set(Dummy.foo()));
+ thread.start();
+ thread.join();
+ assertEquals("foo", reference.get());
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(times(2), Dummy::foo);
+ }
+ }
+
+ @Test
+ public void testStaticMockCanCoexistWithMockInDifferentThread() throws InterruptedException {
+ try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) {
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(Dummy::foo);
+ AtomicReference reference = new AtomicReference<>();
+ Thread thread = new Thread(() -> {
+ try (MockedStatic dummy2 = Mockito.mockStatic(Dummy.class)) {
+ dummy2.when(Dummy::foo).thenReturn("qux");
+ reference.set(Dummy.foo());
+ }
+ });
+ thread.start();
+ thread.join();
+ assertEquals("qux", reference.get());
+ dummy.when(Dummy::foo).thenReturn("bar");
+ assertEquals("bar", Dummy.foo());
+ dummy.verify(times(2), Dummy::foo);
+ }
+ }
+
+ @Test(expected = MockitoException.class)
+ public void testStaticMockMustBeExclusiveInScopeWithinThread() {
+ try (
+ MockedStatic dummy = Mockito.mockStatic(Dummy.class);
+ MockedStatic duplicate = Mockito.mockStatic(Dummy.class)
+ ) {
+ fail("Not supposed to allow duplicates");
+ }
+ }
+
+ static class Dummy {
+
+ static String foo() {
+ return "foo";
+ }
+ }
+}
diff --git a/subprojects/junit-jupiter/src/main/java/org/mockito/junit/jupiter/MockitoExtension.java b/subprojects/junit-jupiter/src/main/java/org/mockito/junit/jupiter/MockitoExtension.java
index 32609c38b4..c2cf37d5fe 100644
--- a/subprojects/junit-jupiter/src/main/java/org/mockito/junit/jupiter/MockitoExtension.java
+++ b/subprojects/junit-jupiter/src/main/java/org/mockito/junit/jupiter/MockitoExtension.java
@@ -4,13 +4,14 @@
*/
package org.mockito.junit.jupiter;
-
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.create;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
import java.lang.reflect.Parameter;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
@@ -20,6 +21,7 @@
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.internal.configuration.MockAnnotationProcessor;
@@ -117,7 +119,7 @@ public class MockitoExtension implements BeforeEachCallback, AfterEachCallback,
private final static Namespace MOCKITO = create("org.mockito");
- private final static String SESSION = "session";
+ private final static String SESSION = "session", MOCKS = "mocks";
private final Strictness strictness;
@@ -150,6 +152,7 @@ public void beforeEach(final ExtensionContext context) {
.logger(new MockitoSessionLoggerAdapter(Plugins.getMockitoLogger()))
.startMocking();
+ context.getStore(MOCKITO).put(MOCKS, new HashSet<>());
context.getStore(MOCKITO).put(SESSION, session);
}
@@ -176,19 +179,30 @@ private Optional retrieveAnnotationFromTestClasses(final Extens
* @param context the current extension context; never {@code null}
*/
@Override
+ @SuppressWarnings("unchecked")
public void afterEach(ExtensionContext context) {
+ context.getStore(MOCKITO).remove(MOCKS, Set.class).forEach(mock -> ((MockedStatic>) mock).closeOnDemand());
context.getStore(MOCKITO).remove(SESSION, MockitoSession.class)
.finishMocking(context.getExecutionException().orElse(null));
}
@Override
- public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+ public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException {
return parameterContext.isAnnotated(Mock.class);
}
@Override
- public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
+ @SuppressWarnings("unchecked")
+ public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException {
final Parameter parameter = parameterContext.getParameter();
- return MockAnnotationProcessor.processAnnotationForMock(parameterContext.findAnnotation(Mock.class).get(), parameter.getType(), parameter.getName());
+ Object mock = MockAnnotationProcessor.processAnnotationForMock(
+ parameterContext.findAnnotation(Mock.class).get(),
+ parameter.getType(),
+ parameter::getParameterizedType,
+ parameter.getName());
+ if (mock instanceof MockedStatic>) {
+ context.getStore(MOCKITO).get(MOCKS, Set.class).add(mock);
+ }
+ return mock;
}
}