Skip to content

Commit 75cfa35

Browse files
committed
Implement createSingletonMock in InlineDexmakerMockMaker
Add singleton mock support to dexmaker, enabling Mockito's mockSingleton() API on Android. Singleton mocks are thread-local and intercept method calls on existing instances without creating new proxy objects.
1 parent d9278b2 commit 75cfa35

6 files changed

Lines changed: 249 additions & 13 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (C) 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.android.dx.mockito.inline.tests;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertSame;
21+
import static org.mockito.Mockito.mockSingleton;
22+
import static org.mockito.Mockito.reset;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
26+
import androidx.test.runner.AndroidJUnit4;
27+
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
import org.mockito.MockedSingleton;
31+
32+
@RunWith(AndroidJUnit4.class)
33+
public class MockSingleton {
34+
35+
enum SingleEnum {
36+
VALUE_A {
37+
@Override
38+
String greeting() {
39+
return "hello";
40+
}
41+
},
42+
VALUE_B {
43+
@Override
44+
String greeting() {
45+
return "world";
46+
}
47+
};
48+
49+
abstract String greeting();
50+
51+
int compute(int x) {
52+
return x + 1;
53+
}
54+
}
55+
56+
static class SimpleSingleton {
57+
static final SimpleSingleton INSTANCE = new SimpleSingleton();
58+
59+
String getValue() {
60+
return "original";
61+
}
62+
63+
int add(int a, int b) {
64+
return a + b;
65+
}
66+
}
67+
68+
@Test
69+
public void mockEnumSingleton() {
70+
assertEquals("hello", SingleEnum.VALUE_A.greeting());
71+
72+
try (MockedSingleton<SingleEnum> mocked = mockSingleton(SingleEnum.VALUE_A)) {
73+
when(SingleEnum.VALUE_A.greeting()).thenReturn("mocked");
74+
assertEquals("mocked", SingleEnum.VALUE_A.greeting());
75+
}
76+
77+
assertEquals("hello", SingleEnum.VALUE_A.greeting());
78+
}
79+
80+
@Test
81+
public void mockEnumDoesNotAffectOtherValues() {
82+
try (MockedSingleton<SingleEnum> mocked = mockSingleton(SingleEnum.VALUE_A)) {
83+
when(SingleEnum.VALUE_A.greeting()).thenReturn("mocked");
84+
85+
assertEquals("mocked", SingleEnum.VALUE_A.greeting());
86+
assertEquals("world", SingleEnum.VALUE_B.greeting());
87+
}
88+
}
89+
90+
@Test
91+
public void mockSingletonInstance() {
92+
assertEquals("original", SimpleSingleton.INSTANCE.getValue());
93+
94+
try (MockedSingleton<SimpleSingleton> mocked = mockSingleton(SimpleSingleton.INSTANCE)) {
95+
when(SimpleSingleton.INSTANCE.getValue()).thenReturn("mocked");
96+
assertEquals("mocked", SimpleSingleton.INSTANCE.getValue());
97+
}
98+
99+
assertEquals("original", SimpleSingleton.INSTANCE.getValue());
100+
}
101+
102+
@Test
103+
public void getInstanceReturnsSameObject() {
104+
try (MockedSingleton<SingleEnum> mocked = mockSingleton(SingleEnum.VALUE_A)) {
105+
assertSame(SingleEnum.VALUE_A, mocked.getInstance());
106+
}
107+
}
108+
109+
@Test
110+
public void resetClearsStubs() {
111+
try (MockedSingleton<SingleEnum> mocked = mockSingleton(SingleEnum.VALUE_A)) {
112+
when(SingleEnum.VALUE_A.greeting()).thenReturn("mocked");
113+
assertEquals("mocked", SingleEnum.VALUE_A.greeting());
114+
115+
reset(SingleEnum.VALUE_A);
116+
// After reset, default mock behavior returns null for objects
117+
assertEquals(null, SingleEnum.VALUE_A.greeting());
118+
}
119+
}
120+
121+
@Test
122+
public void verifyInteraction() {
123+
try (MockedSingleton<SimpleSingleton> mocked = mockSingleton(SimpleSingleton.INSTANCE)) {
124+
SimpleSingleton.INSTANCE.getValue();
125+
126+
verify(SimpleSingleton.INSTANCE).getValue();
127+
}
128+
}
129+
130+
@Test
131+
public void stubMethodWithArgs() {
132+
try (MockedSingleton<SimpleSingleton> mocked = mockSingleton(SimpleSingleton.INSTANCE)) {
133+
when(SimpleSingleton.INSTANCE.add(2, 3)).thenReturn(99);
134+
135+
assertEquals(99, SimpleSingleton.INSTANCE.add(2, 3));
136+
assertEquals(0, SimpleSingleton.INSTANCE.add(1, 1));
137+
}
138+
139+
assertEquals(2, SimpleSingleton.INSTANCE.add(1, 1));
140+
}
141+
}

dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/ClassTransformer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,12 @@ class ClassTransformer {
9494
* mocked or not.
9595
*/
9696
ClassTransformer(JvmtiAgent agent, Class dispatcherClass,
97-
Map<Object, InvocationHandlerAdapter> mocks) {
97+
Map<Object, InvocationHandlerAdapter> mocks,
98+
ThreadLocal<Map<Object, InvocationHandlerAdapter>> singletonMocks) {
9899
this.agent = agent;
99100
mockedTypes = Collections.synchronizedSet(new HashSet<Class<?>>());
100101
identifier = String.valueOf(System.identityHashCode(this));
101-
MockMethodAdvice advice = new MockMethodAdvice(mocks);
102+
MockMethodAdvice advice = new MockMethodAdvice(mocks, singletonMocks);
102103

103104
try {
104105
dispatcherClass.getMethod("set", String.class, Object.class).invoke(null, identifier,

dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/InlineDexmakerMockMaker.java

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@
4444
import java.lang.reflect.Proxy;
4545
import java.util.AbstractMap;
4646
import java.util.Collection;
47+
import java.util.Collections;
4748
import java.util.HashMap;
4849
import java.util.HashSet;
50+
import java.util.IdentityHashMap;
4951
import java.util.Map;
5052
import java.util.Set;
5153

@@ -167,6 +169,9 @@ public final class InlineDexmakerMockMaker implements InlineMockMaker {
167169
*/
168170
private final Map<Object, InvocationHandlerAdapter> mocks;
169171

172+
private final ThreadLocal<Map<Object, InvocationHandlerAdapter>> singletonMocks =
173+
new ThreadLocal<>();
174+
170175
/**
171176
* Class doing the actual byte code transformation.
172177
*/
@@ -185,7 +190,7 @@ public InlineDexmakerMockMaker() {
185190
}
186191

187192
mocks = new MockMap();
188-
classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks);
193+
classTransformer = new ClassTransformer(AGENT, DISPATCHER_CLASS, mocks, singletonMocks);
189194
}
190195

191196
/**
@@ -362,6 +367,54 @@ public MockHandler getHandler(Object mock) {
362367
return adapter != null ? adapter.getHandler() : null;
363368
}
364369

370+
@Override
371+
public <T> SingletonMockControl<T> createSingletonMock(T instance, MockCreationSettings<T> settings, MockHandler handler) {
372+
Class<T> typeToMock = settings.getTypeToMock();
373+
Set<Class<?>> interfacesSet = settings.getExtraInterfaces();
374+
375+
classTransformer.mockClass(MockFeatures.withMockFeatures(typeToMock, interfacesSet));
376+
377+
InvocationHandlerAdapter handlerAdapter = new InvocationHandlerAdapter(handler);
378+
379+
return new InlineSingletonMockControl<>(instance, handlerAdapter);
380+
}
381+
382+
private class InlineSingletonMockControl<T> implements SingletonMockControl<T> {
383+
private final T instance;
384+
private final InvocationHandlerAdapter handlerAdapter;
385+
386+
InlineSingletonMockControl(T instance, InvocationHandlerAdapter handlerAdapter) {
387+
this.instance = instance;
388+
this.handlerAdapter = handlerAdapter;
389+
}
390+
391+
@Override
392+
public T getInstance() {
393+
return instance;
394+
}
395+
396+
@Override
397+
public void enable() {
398+
Map<Object, InvocationHandlerAdapter> singletons = singletonMocks.get();
399+
if (singletons == null) {
400+
singletons = new IdentityHashMap<>();
401+
singletonMocks.set(singletons);
402+
}
403+
singletons.put(instance, handlerAdapter);
404+
}
405+
406+
@Override
407+
public void disable() {
408+
Map<Object, InvocationHandlerAdapter> singletons = singletonMocks.get();
409+
if (singletons != null) {
410+
singletons.remove(instance);
411+
if (singletons.isEmpty()) {
412+
singletonMocks.remove();
413+
}
414+
}
415+
}
416+
}
417+
365418
/**
366419
* Get the {@link InvocationHandlerAdapter} registered for a mock.
367420
*
@@ -374,7 +427,14 @@ private InvocationHandlerAdapter getInvocationHandlerAdapter(Object instance) {
374427
return null;
375428
}
376429

377-
return mocks.get(instance);
430+
InvocationHandlerAdapter adapter = mocks.get(instance);
431+
if (adapter == null) {
432+
Map<Object, InvocationHandlerAdapter> singletons = singletonMocks.get();
433+
if (singletons != null) {
434+
adapter = singletons.get(instance);
435+
}
436+
}
437+
return adapter;
378438
}
379439

380440
/**

dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/JvmtiAgent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class JvmtiAgent {
8080
private static String resolveAgentPath(ClassLoader cl) throws IOException {
8181
String path = ((BaseDexClassLoader) cl).findLibrary("dexmakerjvmtiagent");
8282

83-
if (path != null && !path.contains("=") && !path.contains("!")) {
83+
if (path != null && !path.contains("=")) {
8484
return path;
8585
}
8686

dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMakerMultiplexer.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818

1919
import android.util.Log;
2020

21+
import org.mockito.exceptions.base.MockitoException;
2122
import org.mockito.invocation.MockHandler;
2223
import org.mockito.mock.MockCreationSettings;
2324
import org.mockito.plugins.InlineMockMaker;
2425
import org.mockito.plugins.MockMaker;
2526

27+
import java.lang.reflect.InvocationTargetException;
2628
import java.util.ArrayList;
2729

2830
/**
@@ -35,7 +37,7 @@ public final class MockMakerMultiplexer implements InlineMockMaker {
3537
static {
3638
String[] potentialMockMakers = new String[] {
3739
"com.android.dx.mockito.inline.InlineStaticMockMaker",
38-
"com.android.dx.mockito.inline.InlineDexmakerMockMaker"
40+
InlineDexmakerMockMaker.class.getName()
3941
};
4042

4143
ArrayList<MockMaker> mockMakers = new ArrayList<>();
@@ -44,10 +46,14 @@ public final class MockMakerMultiplexer implements InlineMockMaker {
4446
Class<? extends MockMaker> mockMakerClass = (Class<? extends MockMaker>)
4547
Class.forName(potentialMockMaker);
4648
mockMakers.add(mockMakerClass.getDeclaredConstructor().newInstance());
47-
} catch (Exception e) {
48-
Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e);
49-
} catch (Error e) {
50-
Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e);
49+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException
50+
| NoSuchMethodException | InvocationTargetException e) {
51+
if (potentialMockMaker.equals(InlineDexmakerMockMaker.class.getName())) {
52+
Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker, e);
53+
} else {
54+
// Additional mock makers might not be loaded
55+
Log.e(LOG_TAG, "Could not init mockmaker " + potentialMockMaker);
56+
}
5157
}
5258
}
5359

@@ -133,4 +139,18 @@ public void clearAllMocks() {
133139
inlineMockMaker.clearAllMocks();
134140
}
135141
}
142+
143+
@Override
144+
public <T> SingletonMockControl<T> createSingletonMock(
145+
T instance, MockCreationSettings<T> settings, MockHandler handler) {
146+
for (MockMaker mockMaker : MOCK_MAKERS) {
147+
try {
148+
return mockMaker.createSingletonMock(instance, settings, handler);
149+
} catch (MockitoException ignored) {
150+
}
151+
}
152+
throw new MockitoException(
153+
"The used MockMaker MockMakerMultiplexer does not support the creation of "
154+
+ "singleton mocks\n\nEnsure your MockMaker implementation supports this feature.");
155+
}
136156
}

dexmaker-mockito-inline/src/main/java/com/android/dx/mockito/inline/MockMethodAdvice.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*/
2323
class MockMethodAdvice {
2424
private final Map<Object, InvocationHandlerAdapter> interceptors;
25+
private final ThreadLocal<Map<Object, InvocationHandlerAdapter>> singletonInterceptors;
2526

2627
/** Pattern to decompose a instrumentedMethodWithTypeAndSignature */
2728
private final Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)");
@@ -32,8 +33,21 @@ class MockMethodAdvice {
3233
@SuppressWarnings("ThreadLocalUsage")
3334
private final SelfCallInfo selfCallInfo = new SelfCallInfo();
3435

35-
MockMethodAdvice(Map<Object, InvocationHandlerAdapter> interceptors) {
36+
MockMethodAdvice(Map<Object, InvocationHandlerAdapter> interceptors,
37+
ThreadLocal<Map<Object, InvocationHandlerAdapter>> singletonInterceptors) {
3638
this.interceptors = interceptors;
39+
this.singletonInterceptors = singletonInterceptors;
40+
}
41+
42+
private InvocationHandlerAdapter getInterceptor(Object instance) {
43+
InvocationHandlerAdapter interceptor = interceptors.get(instance);
44+
if (interceptor == null && singletonInterceptors != null) {
45+
Map<Object, InvocationHandlerAdapter> singletons = singletonInterceptors.get();
46+
if (singletons != null) {
47+
interceptor = singletons.get(instance);
48+
}
49+
}
50+
return interceptor;
3751
}
3852

3953
/**
@@ -225,7 +239,7 @@ public Method getOrigin(Object instance, String methodWithTypeAndSignature) thro
225239
*/
226240
@SuppressWarnings("unused")
227241
public Callable<?> handle(Object instance, Method origin, Object[] arguments) throws Throwable {
228-
InvocationHandlerAdapter interceptor = interceptors.get(instance);
242+
InvocationHandlerAdapter interceptor = getInterceptor(instance);
229243
if (interceptor == null) {
230244
return null;
231245
}
@@ -242,7 +256,7 @@ public Callable<?> handle(Object instance, Method origin, Object[] arguments) th
242256
* @return {@code true} iff the instance is a mock
243257
*/
244258
public boolean isMock(Object instance) {
245-
return interceptors.containsKey(instance);
259+
return getInterceptor(instance) != null;
246260
}
247261

248262
/**

0 commit comments

Comments
 (0)