From c374a853f28be629dc53697a61124c5796898c3f Mon Sep 17 00:00:00 2001 From: Rafael Winterhalter Date: Fri, 19 Jun 2020 11:12:31 +0200 Subject: [PATCH] Mockito \#1013: Defines and implements API for static mocking. --- src/main/java/org/mockito/Mock.java | 1 + src/main/java/org/mockito/MockSettings.java | 15 ++ src/main/java/org/mockito/MockedStatic.java | 209 ++++++++++++++++++ src/main/java/org/mockito/Mockito.java | 91 ++++++++ .../java/org/mockito/MockitoAnnotations.java | 15 +- .../org/mockito/internal/MockitoCore.java | 19 ++ .../IndependentAnnotationEngine.java | 17 +- .../InjectingAnnotationEngine.java | 41 +++- .../MockAnnotationProcessor.java | 38 +++- .../configuration/SpyAnnotationEngine.java | 3 +- .../internal/creation/MockSettingsImpl.java | 30 ++- .../internal/creation/StaticMockControl.java | 17 ++ .../bytebuddy/ByteBuddyMockMaker.java | 7 + .../creation/bytebuddy/BytecodeGenerator.java | 2 + .../bytebuddy/InlineByteBuddyMockMaker.java | 123 ++++++++++- .../bytebuddy/InlineBytecodeGenerator.java | 72 ++++-- .../creation/bytebuddy/MockMethodAdvice.java | 95 +++++++- .../bytebuddy/SubclassByteBuddyMockMaker.java | 12 + .../bytebuddy/SubclassBytecodeGenerator.java | 5 + .../TypeCachingBytecodeGenerator.java | 5 + .../inject/MockMethodDispatcher.java | 27 ++- .../framework/DefaultMockitoSession.java | 26 ++- .../invocation/InterceptedInvocation.java | 12 - .../internal/junit/JUnitSessionStore.java | 7 +- .../runners/DefaultInternalRunner.java | 13 +- .../mockito/internal/util/MockNameImpl.java | 13 +- .../org/mockito/internal/util/MockUtil.java | 7 + .../util/concurrent/WeakConcurrentSet.java | 2 +- .../org/mockito/plugins/AnnotationEngine.java | 8 +- .../java/org/mockito/plugins/MockMaker.java | 21 ++ src/test/java/org/mockito/MockitoTest.java | 7 + .../GlobalConfigurationTest.java | 4 +- .../internal/util/MockNameImplTest.java | 32 ++- .../creation/AndroidByteBuddyMockMaker.java | 11 + .../MockitoAnyClassWithPrimitiveType.java | 4 +- .../org/mockitoinline/StaticMockTest.java | 175 +++++++++++++++ .../junit/jupiter/MockitoExtension.java | 24 +- 37 files changed, 1124 insertions(+), 86 deletions(-) create mode 100644 src/main/java/org/mockito/MockedStatic.java create mode 100644 src/main/java/org/mockito/internal/creation/StaticMockControl.java create mode 100644 subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java 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..d55fa5ca2e
    --- /dev/null
    +++ b/src/main/java/org/mockito/MockedStatic.java
    @@ -0,0 +1,209 @@
    +/*
    + * 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;
    +
    +/**
    + * Represents an active mock of a type's static methods. The mocking only affects the thread
    + * on which this static mock was created and it is not safe to use this object from another
    + * thread. The static mock is released when this object's {@link MockedStatic#close()} method
    + * is invoked. If this object is never closed, the static mock will remain active on the
    + * initiating thread. It is therefore recommended to create this object within a try-with-resources
    + * statement unless when managed explicitly, for example by using a JUnit rule or extension.
    + * 

    + * If the {@link Mock} annotation is used on fields or method parameters of this type, a static mock + * is created instead of a regular mock. The static mock is activated and released upon completing any + * relevant test. + * + * @param The type being mocked. + */ +@Incubating +public final class MockedStatic implements AutoCloseable { + + private final StaticMockControl control; + + private boolean closed; + + private final Location location = new LocationImpl(); + + public MockedStatic(StaticMockControl control) { + this.control = control; + } + + /** + * See {@link Mockito#when(Object)}. + */ + 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; + } + + /** + * See {@link Mockito#verify(Object)}. + */ + public void verify(Verification verification) { + verify(times(1), verification); + } + + /** + * See {@link Mockito#verify(Object, VerificationMode)}. + */ + 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); + } + } + + /** + * See {@link Mockito#reset(Object[])}. + */ + public void reset() { + assertNotClosed(); + + MockingProgress mockingProgress = mockingProgress(); + mockingProgress.validateState(); + mockingProgress.reset(); + mockingProgress.resetOngoingStubbing(); + + resetMock(control.getType()); + } + + /** + * See {@link Mockito#clearInvocations(Object[])}. + */ + public void clearInvocations() { + assertNotClosed(); + + MockingProgress mockingProgress = mockingProgress(); + mockingProgress.validateState(); + mockingProgress.reset(); + mockingProgress.resetOngoingStubbing(); + + getInvocationContainer(control.getType()).clearInvocations(); + } + + /** + * {@link Mockito#verifyNoMoreInteractions(Object...)}. + */ + public void verifyNoMoreInteractions() { + assertNotClosed(); + + mockingProgress().validateState(); + InvocationContainerImpl invocations = getInvocationContainer(control.getType()); + VerificationDataImpl data = new VerificationDataImpl(invocations, null); + noMoreInteractions().verify(data); + } + + /** + * See {@link Mockito#verifyNoInteractions(Object...)}. + */ + 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(); + } + + /** + * Releases this static mock and is non-operational if already released. + */ + 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..4ea419d886 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)
    * * *

    0. Migrating to Mockito 2

    @@ -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. + * + * + *

    48. Mocking static methods (since 3.4.0)

    + * + * 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..0f63acc765 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..2f6667015c 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,41 @@ 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 +88,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 +102,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..c7f562a830 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,12 @@ 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 +50,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..da0321545d 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,34 @@ 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..1a7bff3fd5 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,10 @@ 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 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..06840d26ee 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 { @@ -300,19 +314,41 @@ public MockHandler getHandler(Object mock) { 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); + 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 +375,79 @@ 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..187f1ea945 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,8 @@ 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 +191,46 @@ public Class 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 +243,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 +310,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 +327,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..a7f3ab998f 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,24 @@ 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 +156,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 +260,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 +387,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..54a6573b6f 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,15 @@ 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 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..f1b49051e0 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,42 @@ 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..66bee0beac 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,10 @@ 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..c33768ded7 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..29f3c7d77b 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,26 @@ 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..2dde8d4a9b --- /dev/null +++ b/subprojects/inline/src/test/java/org/mockitoinline/StaticMockTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitoinline; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.fail; +import static org.mockito.Mockito.times; + +import java.util.concurrent.atomic.AtomicReference; + +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; + +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 testStaticMockWithRealMethodCall() { + try (MockedStatic dummy = Mockito.mockStatic(Dummy.class)) { + dummy.when(Dummy::foo).thenCallRealMethod(); + assertEquals("foo", 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; } }