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 19, 2020
1 parent 4fd405d commit 03ef24a
Show file tree
Hide file tree
Showing 37 changed files with 1,046 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
168 changes: 168 additions & 0 deletions src/main/java/org/mockito/MockedStatic.java
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2007 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito;

import org.mockito.exceptions.base.MockitoAssertionError;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.creation.StaticMockControl;
import org.mockito.internal.debugging.LocationImpl;
import org.mockito.internal.listeners.VerificationStartedNotifier;
import org.mockito.internal.progress.MockingProgress;
import org.mockito.internal.stubbing.InvocationContainerImpl;
import org.mockito.internal.verification.MockAwareVerificationMode;
import org.mockito.internal.verification.VerificationDataImpl;
import org.mockito.invocation.Location;
import org.mockito.invocation.MockHandler;
import org.mockito.stubbing.OngoingStubbing;
import org.mockito.verification.VerificationMode;

import static org.mockito.Mockito.times;
import static org.mockito.internal.exceptions.Reporter.missingMethodInvocation;
import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
import static org.mockito.internal.util.MockUtil.getInvocationContainer;
import static org.mockito.internal.util.MockUtil.resetMock;
import static org.mockito.internal.util.StringUtil.join;
import static org.mockito.internal.verification.VerificationModeFactory.noInteractions;
import static org.mockito.internal.verification.VerificationModeFactory.noMoreInteractions;

@Incubating
public class MockedStatic<T> implements AutoCloseable {

private final StaticMockControl<T> control;

private boolean closed;

private final Location location = new LocationImpl();

public MockedStatic(StaticMockControl<T> control) {
this.control = control;
}

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;
}

public void verify(Verification verification) {
verify(times(1), verification);
}

public void verify(VerificationMode mode, Verification verification) {
assertNotClosed();

MockingDetails mockingDetails = Mockito.mockingDetails(control.getType());
MockHandler handler = mockingDetails.getMockHandler();

VerificationStartedNotifier.notifyVerificationStarted(
handler.getMockSettings().getVerificationStartedListeners(),
mockingDetails);

MockingProgress mockingProgress = mockingProgress();
VerificationMode actualMode = mockingProgress.maybeVerifyLazily(mode);
mockingProgress.verificationStarted(
new MockAwareVerificationMode(
control.getType(), actualMode, mockingProgress.verificationListeners()));

try {
verification.apply();
} catch (MockitoException | MockitoAssertionError e) {
throw e;
} catch (Throwable t) {
throw new MockitoException(join(
"An unexpected error occurred while verifying a static stub",
"",
"To correctly verify a stub, invoke a single static method of " + control.getType().getName() + " in the provided lambda.",
"For example, if a method 'sample' was defined, provide a lambda or anonymous class containing the code",
"",
"() -> " + control.getType().getSimpleName() + ".sample()",
"or",
control.getType().getSimpleName() + "::sample"
), t);
}
}

public void reset() {
assertNotClosed();

MockingProgress mockingProgress = mockingProgress();
mockingProgress.validateState();
mockingProgress.reset();
mockingProgress.resetOngoingStubbing();

resetMock(control.getType());
}

public void clearInvocations() {
assertNotClosed();

MockingProgress mockingProgress = mockingProgress();
mockingProgress.validateState();
mockingProgress.reset();
mockingProgress.resetOngoingStubbing();

getInvocationContainer(control.getType()).clearInvocations();
}

public void verifyNoMoreInteractions() {
assertNotClosed();

mockingProgress().validateState();
InvocationContainerImpl invocations = getInvocationContainer(control.getType());
VerificationDataImpl data = new VerificationDataImpl(invocations, null);
noMoreInteractions().verify(data);
}

public void verifyNoInteractions() {
assertNotClosed();

mockingProgress().validateState();
InvocationContainerImpl invocations = getInvocationContainer(control.getType());
VerificationDataImpl data = new VerificationDataImpl(invocations, null);
noInteractions().verify(data);
}

@Override
public void close() {
assertNotClosed();

closed = true;
control.disable();
}

public void closeOnDemand() {
if (!closed) {
close();
}
}

private void assertNotClosed() {
if (closed) {
throw new MockitoException(join(
"The static mock created at",
location.toString(),
"is already resolved and cannot longer be used"
));
}
}

@FunctionalInterface
public interface Verification {

void apply() throws Throwable;
}
}
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);
}
}
19 changes: 19 additions & 0 deletions src/main/java/org/mockito/internal/MockitoCore.java
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -68,6 +71,22 @@ public <T> T mock(Class<T> typeToMock, MockSettings settings) {
return mock;
}

public <T> MockedStatic<T> mockStatic(Class<T> 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<T> creationSettings = impl.buildStatic(classToMock);
StaticMockControl<T> control = createStaticMock(classToMock, creationSettings);
control.enable();
mockingProgress().mockingStarted(classToMock, creationSettings);
return new MockedStatic<>(control);
}

public <T> OngoingStubbing<T> when(T methodCall) {
MockingProgress mockingProgress = mockingProgress();
mockingProgress.stubbingStarted();
Expand Down

0 comments on commit 03ef24a

Please sign in to comment.