Skip to content

Commit

Permalink
Mockito \#1013: Defines and implements API for static mocking.
Browse files Browse the repository at this point in the history
  • Loading branch information
raphw committed Jun 21, 2020
1 parent 4fd405d commit d585a4f
Show file tree
Hide file tree
Showing 37 changed files with 1,129 additions and 88 deletions.
1 change: 1 addition & 0 deletions src/main/java/org/mockito/Mock.java
Expand Up @@ -23,6 +23,7 @@
* <li>Minimizes repetitive mock creation code.</li>
* <li>Makes the test class more readable.</li>
* <li>Makes the verification error easier to read because the <b>field name</b> is used to identify the mock.</li>
* <li>Automatically detects static mocks of type {@link MockedStatic} and infers the static mock type of the type parameter.</li>
* </ul>
*
* <pre class="code"><code class="java">
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/mockito/MockSettings.java
Expand Up @@ -340,6 +340,21 @@ public interface MockSettings extends Serializable {
@Incubating
<T> MockCreationSettings<T> build(Class<T> 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 <T> type to mock
* @return immutable view of mock settings
* @since 2.10.0
*/
@Incubating
<T> MockCreationSettings<T> buildStatic(Class<T> 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
Expand Down
209 changes: 209 additions & 0 deletions 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.
* <p>
* 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 <T> The type being mocked.
*/
@Incubating
public final class MockedStatic<T> implements AutoCloseable {

This comment has been minimized.

Copy link
@ChristianSchwarz

ChristianSchwarz Jun 24, 2020

Contributor

@raphw Why do we need the type parameter ?

This comment has been minimized.

Copy link
@raphw

raphw Jul 1, 2020

Author Member

For being able to use this as an instance in JUnit runners, for example where the static value is inferred. If not needed, you can just set a wildcard.

This comment has been minimized.

Copy link
@ChristianSchwarz

ChristianSchwarz Jul 2, 2020

Contributor

I am not sure if I understand it fully. Given your example:

@Mock 
MockedStatic<Foo> foo;

How the the type parameter <Foo> can be retrieved, it will be erased at runtime?

The type parameter is only used in StaticMockControl.getType() , for my understanding it is sufficient to return Class<?> from it.

This comment has been minimized.

Copy link
@raphw

raphw Jul 2, 2020

Author Member

Generic information is not erased for signatures, i.e. fields, methods and subtypes. It is however ignored by the JVM itself which is why you can only refer to generic types by their erasure in byte code instructions.

This comment has been minimized.

Copy link
@ChristianSchwarz

ChristianSchwarz Jul 2, 2020

Contributor

Okay now I understand field.getGenericType() returns the required type informations.


private final StaticMockControl<T> control;

private boolean closed;

private final Location location = new LocationImpl();

public MockedStatic(StaticMockControl<T> control) {

This comment has been minimized.

Copy link
@ChristianSchwarz

ChristianSchwarz Jul 2, 2020

Contributor

StaticMockControl is internal but leaked into the public API here. I think MockedStatic should better be an interface.

This comment has been minimized.

Copy link
@raphw

raphw Jul 2, 2020

Author Member

Agreed, I factored it out to an interface.

this.control = control;
}

/**
* See {@link Mockito#when(Object)}.
*/
public <S> OngoingStubbing<S> when(Verification verification) {
assertNotClosed();

try {
verification.apply();
} catch (Throwable ignored) {
}

MockingProgress mockingProgress = mockingProgress();
mockingProgress.stubbingStarted();
@SuppressWarnings("unchecked")
OngoingStubbing<S> stubbing = (OngoingStubbing<S>) 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;
}
}
91 changes: 91 additions & 0 deletions src/main/java/org/mockito/Mockito.java
Expand Up @@ -105,6 +105,7 @@
* <a href="#45">45. New JUnit Jupiter (JUnit5+) extension</a><br/>
* <a href="#46">46. New <code>Mockito.lenient()</code> and <code>MockSettings.lenient()</code> methods (Since 2.20.0)</a><br/>
* <a href="#47">47. New API for clearing mock state in inline mocking (Since 2.25.0)</a><br/>
* <a href="#48">48. New API for mocking static methods (Since 3.4.0)</a><br/>
* </b>
*
* <h3 id="0">0. <a class="meaningful_link" href="#mockito2" name="mockito2">Migrating to Mockito 2</a></h3>
Expand Down Expand Up @@ -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.
*
*
* <h3 id="48">48. <a class="meaningful_link" href="#static_mocks" name="static_mocks">Mocking static methods</a> (since 3.4.0)</h3>
*
* When using the <a href="#0.2">inline mock maker</a>, 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 <code>Foo</code> type's static method would return <code>foo</code> unless mocked:
*
* <pre class="code"><code class="java">
* assertEquals("foo", Foo.method());
* try (MockedStatic<Foo> mocked = mockStatic(Foo.class)) {
* mocked.when(Foo::method).thenReturn("bar");
* assertEquals("bar", Foo.method());
* mocked.verify(Foo::method);
* }
* assertEquals("foo", Foo.method());
* </code></pre>
*
* 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 <code>MockedStatic</code> that is returned.
* <p>
*/
@SuppressWarnings("unchecked")
public class Mockito extends ArgumentMatchers {
Expand Down Expand Up @@ -2026,6 +2050,73 @@ public static <T> T spy(Class<T> 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.
* <p>
* 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 <T> MockedStatic<T> mockStatic(Class<T> 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.
* <p>
* 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 <T> MockedStatic<T> mockStatic(Class<T> 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.
* <p>
* 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 <T> MockedStatic<T> mockStatic(Class<T> 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.
* <p>
* 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 <T> MockedStatic<T> mockStatic(Class<T> 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.
* <p>
Expand Down
15 changes: 11 additions & 4 deletions src/main/java/org/mockito/MockitoAnnotations.java
Expand Up @@ -37,8 +37,14 @@
*
* public class SampleBaseTestCase {
*
* private AutoCloseable closeable;
*
* &#064;Before public void initMocks() {
* MockitoAnnotations.initMocks(this);
* closeable = MockitoAnnotations.initMocks(this);
* }
*
* &#064;After public void releaseMocks() throws Exception {
* closeable.close();
* }
* }
* </code></pre>
Expand All @@ -49,7 +55,8 @@
* <p>
* In above example, <code>initMocks()</code> is called in &#064;Before (JUnit4) method of test's base class.
* For JUnit3 <code>initMocks()</code> can go to <code>setup()</code> method of a base class.
* You can also put initMocks() in your JUnit runner (&#064;RunWith) or use built-in runner: {@link MockitoJUnitRunner}
* You can also put initMocks() in your JUnit runner (&#064;RunWith) or use built-in runner: {@link MockitoJUnitRunner}.
* If static method mocks are used, it is required to close the initialization.
*/
public class MockitoAnnotations {

Expand All @@ -59,14 +66,14 @@ public class MockitoAnnotations {
* <p>
* 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");
}

AnnotationEngine annotationEngine =
new GlobalConfiguration().tryGetPluginAnnotationEngine();
annotationEngine.process(testClass.getClass(), testClass);
return annotationEngine.process(testClass.getClass(), testClass);
}
}

0 comments on commit d585a4f

Please sign in to comment.