Skip to content

Commit 25f1395

Browse files
jselboJoshua Selbo
andauthored
Add core API to enable Kotlin singleton mocking (#3762)
* Add core API to enable Kotlin singleton mocking Fixes #3652 * Update implementation - no modifying MockSettings API. Entry point is via MockUtil in the internal package. * Fix NPE * Reimplement as mockSingleton API, decouple from static mocking, have docs/tests demonstrate stubbing Java enums * Update javadocs * Update wording of MockedSingleton javadoc --------- Co-authored-by: Joshua Selbo <jselbo@meta.com>
1 parent ef9ee55 commit 25f1395

14 files changed

Lines changed: 478 additions & 119 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2007 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
package org.mockito;
6+
7+
/**
8+
* Represents an active thread-local mock of a singleton instance. The mocking only affects the thread
9+
* on which {@link Mockito#mockSingleton(Object)} was called, and the instance only behaves as a mock on that thread.
10+
* The singleton mock is released when this object's {@link #close()} method is invoked. If this object is never closed,
11+
* the mock will remain active on the initiating thread. It is therefore recommended to create this object within a
12+
* try-with-resources statement.
13+
* <p>
14+
* Stubbing and verification on the instance can be done using the standard Mockito APIs.
15+
*
16+
* @see Mockito#mockSingleton(Object)
17+
* @param <T> The type of the singleton being mocked.
18+
*/
19+
public interface MockedSingleton<T> extends ScopedMock {
20+
21+
/**
22+
* Returns the mocked singleton instance.
23+
*/
24+
T getInstance();
25+
}

mockito-core/src/main/java/org/mockito/Mockito.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@
115115
* <a href="#52">52. New strictness attribute for @Mock annotation and <code>MockSettings.strictness()</code> methods (Since 4.6.0)</a><br/>
116116
* <a href="#53">53. Specifying mock maker for individual mocks (Since 4.8.0)</a><br/>
117117
* <a href="#54">54. Mocking/spying without specifying class (Since 4.10.0)</a><br/>
118+
* <a href="#55">55. Verification with assertions (Since 5.3.0)</a><br/>
119+
* <a href="#56">56. Mocking singletons (like Java enums) (Since 5.22.0)</a><br/>
118120
* </b>
119121
*
120122
* <h3 id="0">0. <a class="meaningful_link" href="#mockito2" name="mockito2">Migrating to Mockito 2</a></h3>
@@ -1769,6 +1771,20 @@
17691771
* assertThat(param.getField2()).isEqualTo("bar");
17701772
* }));
17711773
* </code></pre>
1774+
*
1775+
* <h3 id="56">56. <a class="meaningful_link" href="#mocked_singleton" name="mocked_singleton">
1776+
* Mocking singletons (like Java enums)</a> (Since 5.22.0)</h3>
1777+
*
1778+
* Use the new {@link Mockito#mockSingleton(Object)} API to configure thread-local mocking of singleton objects for
1779+
* which you don't control initialization, assignment, or access. Java enums are a good example. The mocking only
1780+
* applies to the given instance and that instance only behaves as a mock on the current thread.
1781+
*
1782+
* <pre class="code"><code class="java">
1783+
* try (MockedSingleton&lt;MyEnum&gt; mocked = mockSingleton(MyEnum.A)) {
1784+
* when(MyEnum.A.method()).thenReturn("bar");
1785+
* assertEquals("bar", MyEnum.A.method());
1786+
* }
1787+
* </code></pre>
17721788
*/
17731789
@CheckReturnValue
17741790
@SuppressWarnings("unchecked")
@@ -2527,6 +2543,43 @@ public static <T> MockedStatic<T> mockStatic(MockSettings mockSettings, T... rei
25272543
return mockStatic(getClassOf(reified), mockSettings);
25282544
}
25292545

2546+
/**
2547+
* Creates a thread-local mock controller for the given singleton instance.
2548+
* The returned object's {@link MockedSingleton#close()} method must be called upon completing the
2549+
* test or the mock will remain active on the current thread.
2550+
* <p>
2551+
* This is useful for mocking instances of objects for which you don't control initialization, assignment, or access to the object, e.g. Java enum values.
2552+
* <p>
2553+
* See examples in javadoc for {@link Mockito} class
2554+
*
2555+
* @param instance the singleton instance to mock.
2556+
* @param <T> the type of the singleton.
2557+
* @return mock controller
2558+
* @since 5.22.0
2559+
*/
2560+
public static <T> MockedSingleton<T> mockSingleton(T instance) {
2561+
return mockSingleton(instance, withSettings());
2562+
}
2563+
2564+
/**
2565+
* Creates a thread-local mock controller for the given singleton instance.
2566+
* The returned object's {@link MockedSingleton#close()} method must be called upon completing the
2567+
* test or the mock will remain active on the current thread.
2568+
* <p>
2569+
* This is useful for mocking instances of objects for which you don't control initialization, assignment, or access to the object, e.g. Java enum values.
2570+
* <p>
2571+
* See examples in javadoc for {@link Mockito} class
2572+
*
2573+
* @param instance the singleton instance to mock.
2574+
* @param mockSettings the mock settings to use.
2575+
* @param <T> the type of the singleton.
2576+
* @return mock controller
2577+
* @since 5.22.0
2578+
*/
2579+
public static <T> MockedSingleton<T> mockSingleton(T instance, MockSettings mockSettings) {
2580+
return MOCKITO_CORE.mockSingleton(instance, mockSettings);
2581+
}
2582+
25302583
/**
25312584
* Creates a thread-local mock controller for all constructions of the given class.
25322585
* The returned object's {@link MockedConstruction#close()} method must be called upon completing the

mockito-core/src/main/java/org/mockito/internal/MockedConstructionImpl.java

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,21 @@
44
*/
55
package org.mockito.internal;
66

7-
import static org.mockito.internal.util.StringUtil.join;
8-
97
import java.util.Collections;
108
import java.util.List;
11-
129
import org.mockito.MockedConstruction;
13-
import org.mockito.exceptions.base.MockitoException;
14-
import org.mockito.internal.debugging.LocationFactory;
15-
import org.mockito.invocation.Location;
1610
import org.mockito.plugins.MockMaker;
1711

18-
public final class MockedConstructionImpl<T> implements MockedConstruction<T> {
19-
20-
private final MockMaker.ConstructionMockControl<T> control;
21-
22-
private boolean closed;
23-
24-
private final Location location = LocationFactory.create();
12+
public final class MockedConstructionImpl<T>
13+
extends ScopedMockImpl<MockMaker.ConstructionMockControl<T>>
14+
implements MockedConstruction<T> {
2515

2616
protected MockedConstructionImpl(MockMaker.ConstructionMockControl<T> control) {
27-
this.control = control;
17+
super(control);
2818
}
2919

3020
@Override
3121
public List<T> constructed() {
3222
return Collections.unmodifiableList(control.getMocks());
3323
}
34-
35-
@Override
36-
public boolean isClosed() {
37-
return closed;
38-
}
39-
40-
@Override
41-
public void close() {
42-
assertNotClosed();
43-
44-
closed = true;
45-
control.disable();
46-
}
47-
48-
@Override
49-
public void closeOnDemand() {
50-
if (!closed) {
51-
close();
52-
}
53-
}
54-
55-
private void assertNotClosed() {
56-
if (closed) {
57-
throw new MockitoException(
58-
join(
59-
"The static mock created at",
60-
location.toString(),
61-
"is already resolved and cannot longer be used"));
62-
}
63-
}
6424
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2007 Mockito contributors
3+
* This program is made available under the terms of the MIT License.
4+
*/
5+
package org.mockito.internal;
6+
7+
import org.mockito.MockedSingleton;
8+
import org.mockito.plugins.MockMaker;
9+
10+
public final class MockedSingletonImpl<T> extends ScopedMockImpl<MockMaker.SingletonMockControl<T>>
11+
implements MockedSingleton<T> {
12+
13+
public MockedSingletonImpl(MockMaker.SingletonMockControl<T> control) {
14+
super(control);
15+
}
16+
17+
@Override
18+
public T getInstance() {
19+
return control.getInstance();
20+
}
21+
22+
@Override
23+
public String toString() {
24+
return "singleton mock for " + control.getInstance();
25+
}
26+
}

mockito-core/src/main/java/org/mockito/internal/MockedStaticImpl.java

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,21 @@
1717
import org.mockito.Mockito;
1818
import org.mockito.exceptions.base.MockitoAssertionError;
1919
import org.mockito.exceptions.base.MockitoException;
20-
import org.mockito.internal.debugging.LocationFactory;
2120
import org.mockito.internal.listeners.VerificationStartedNotifier;
2221
import org.mockito.internal.progress.MockingProgress;
2322
import org.mockito.internal.stubbing.InvocationContainerImpl;
2423
import org.mockito.internal.verification.MockAwareVerificationMode;
2524
import org.mockito.internal.verification.VerificationDataImpl;
26-
import org.mockito.invocation.Location;
2725
import org.mockito.invocation.MockHandler;
2826
import org.mockito.plugins.MockMaker;
2927
import org.mockito.stubbing.OngoingStubbing;
3028
import org.mockito.verification.VerificationMode;
3129

32-
public final class MockedStaticImpl<T> implements MockedStatic<T> {
30+
public final class MockedStaticImpl<T> extends ScopedMockImpl<MockMaker.StaticMockControl<T>>
31+
implements MockedStatic<T> {
3332

34-
private final MockMaker.StaticMockControl<T> control;
35-
36-
private boolean closed;
37-
38-
private final Location location = LocationFactory.create();
39-
40-
protected MockedStaticImpl(MockMaker.StaticMockControl<T> control) {
41-
this.control = control;
33+
public MockedStaticImpl(MockMaker.StaticMockControl<T> control) {
34+
super(control);
4235
}
4336

4437
@Override
@@ -144,36 +137,6 @@ public void verifyNoInteractions() {
144137
noInteractions().verify(data);
145138
}
146139

147-
@Override
148-
public boolean isClosed() {
149-
return closed;
150-
}
151-
152-
@Override
153-
public void close() {
154-
assertNotClosed();
155-
156-
closed = true;
157-
control.disable();
158-
}
159-
160-
@Override
161-
public void closeOnDemand() {
162-
if (!closed) {
163-
close();
164-
}
165-
}
166-
167-
private void assertNotClosed() {
168-
if (closed) {
169-
throw new MockitoException(
170-
join(
171-
"The static mock created at",
172-
location.toString(),
173-
"is already resolved and cannot longer be used"));
174-
}
175-
}
176-
177140
@Override
178141
public String toString() {
179142
return "static mock for " + control.getType().getName();

mockito-core/src/main/java/org/mockito/internal/MockitoCore.java

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static org.mockito.internal.progress.ThreadSafeMockingProgress.mockingProgress;
1818
import static org.mockito.internal.util.MockUtil.createConstructionMock;
1919
import static org.mockito.internal.util.MockUtil.createMock;
20+
import static org.mockito.internal.util.MockUtil.createSingletonMock;
2021
import static org.mockito.internal.util.MockUtil.createStaticMock;
2122
import static org.mockito.internal.util.MockUtil.getInvocationContainer;
2223
import static org.mockito.internal.util.MockUtil.getMockHandler;
@@ -32,6 +33,7 @@
3233
import org.mockito.InOrder;
3334
import org.mockito.MockSettings;
3435
import org.mockito.MockedConstruction;
36+
import org.mockito.MockedSingleton;
3537
import org.mockito.MockedStatic;
3638
import org.mockito.MockingDetails;
3739
import org.mockito.exceptions.misusing.DoNotMockException;
@@ -71,14 +73,7 @@ public class MockitoCore {
7173
Plugins.getDoNotMockEnforcer();
7274

7375
public <T> T mock(Class<T> typeToMock, MockSettings settings) {
74-
if (!(settings instanceof MockSettingsImpl)) {
75-
throw new IllegalArgumentException(
76-
"Unexpected implementation of '"
77-
+ settings.getClass().getCanonicalName()
78-
+ "'\n"
79-
+ "At the moment, you cannot provide your own implementations of that class.");
80-
}
81-
MockSettingsImpl impl = (MockSettingsImpl) settings;
76+
MockSettingsImpl impl = castMockSettings(settings);
8277
MockCreationSettings<T> creationSettings = impl.build(typeToMock);
8378
checkDoNotMockAnnotation(creationSettings);
8479
T mock = createMock(creationSettings);
@@ -87,14 +82,7 @@ public <T> T mock(Class<T> typeToMock, MockSettings settings) {
8782
}
8883

8984
public <T> MockedStatic<T> mockStatic(Class<T> classToMock, MockSettings settings) {
90-
if (!MockSettingsImpl.class.isInstance(settings)) {
91-
throw new IllegalArgumentException(
92-
"Unexpected implementation of '"
93-
+ settings.getClass().getCanonicalName()
94-
+ "'\n"
95-
+ "At the moment, you cannot provide your own implementations of that class.");
96-
}
97-
MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
85+
MockSettingsImpl impl = castMockSettings(settings);
9886
MockCreationSettings<T> creationSettings = impl.buildStatic(classToMock);
9987
checkDoNotMockAnnotation(creationSettings);
10088
MockMaker.StaticMockControl<T> control = createStaticMock(classToMock, creationSettings);
@@ -103,6 +91,20 @@ public <T> MockedStatic<T> mockStatic(Class<T> classToMock, MockSettings setting
10391
return new MockedStaticImpl<>(control);
10492
}
10593

94+
@SuppressWarnings("unchecked")
95+
public <T> MockedSingleton<T> mockSingleton(T instance, MockSettings settings) {
96+
if (instance == null) {
97+
throw new IllegalArgumentException("Cannot mock a null instance");
98+
}
99+
MockSettingsImpl impl = castMockSettings(settings);
100+
MockCreationSettings<T> creationSettings = impl.build(instance.getClass());
101+
checkDoNotMockAnnotation(creationSettings);
102+
MockMaker.SingletonMockControl<T> control = createSingletonMock(instance, creationSettings);
103+
control.enable();
104+
mockingProgress().mockingStarted(instance, creationSettings);
105+
return new MockedSingletonImpl<>(control);
106+
}
107+
106108
private void checkDoNotMockAnnotation(MockCreationSettings<?> creationSettings) {
107109
String warning = DO_NOT_MOCK_ENFORCER.checkTypeForDoNotMockViolation(creationSettings);
108110
if (warning != null) {
@@ -116,15 +118,8 @@ public <T> MockedConstruction<T> mockConstruction(
116118
MockedConstruction.MockInitializer<T> mockInitializer) {
117119
Function<MockedConstruction.Context, MockCreationSettings<T>> creationSettings =
118120
context -> {
119-
MockSettings value = settingsFactory.apply(context);
120-
if (!MockSettingsImpl.class.isInstance(value)) {
121-
throw new IllegalArgumentException(
122-
"Unexpected implementation of '"
123-
+ value.getClass().getCanonicalName()
124-
+ "'\n"
125-
+ "At the moment, you cannot provide your own implementations of that class.");
126-
}
127-
MockSettingsImpl impl = MockSettingsImpl.class.cast(value);
121+
MockSettings settings = settingsFactory.apply(context);
122+
MockSettingsImpl impl = castMockSettings(settings);
128123
String mockMaker = impl.getMockMaker();
129124
if (mockMaker != null) {
130125
throw new IllegalArgumentException(
@@ -322,4 +317,15 @@ public LenientStubber lenient() {
322317
public void clearAllCaches() {
323318
MockUtil.clearAllCaches();
324319
}
320+
321+
private MockSettingsImpl<?> castMockSettings(MockSettings settings) {
322+
if (!(settings instanceof MockSettingsImpl)) {
323+
throw new IllegalArgumentException(
324+
"Unexpected implementation of '"
325+
+ settings.getClass().getCanonicalName()
326+
+ "'\n"
327+
+ "At the moment, you cannot provide your own implementations of that class.");
328+
}
329+
return (MockSettingsImpl<?>) settings;
330+
}
325331
}

0 commit comments

Comments
 (0)