Skip to content

Commit 7f93107

Browse files
AndreasTuleonard84
andauthored
Fix SpyStatic() with an interaction closure throws NullPointerException (#2254)
The usage of ```groovy SpyStatic(Type){ } ```` had thrown an `NullPointerException` , because the closure got automatically converted into IMockMakerSettings. This adds a meaningful error message for the user to use `SpyStatic(Type)` without a `Closure` instead of `SpyStatic(Type){}`. --------- Co-authored-by: Leonard Brünings <lbruenings@gradle.com>
1 parent 5f4d8c3 commit 7f93107

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

docs/release_notes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ spockPull:2112[]
113113
* Fix filter blocks with shared fields and derived data variables spockPull:2088[]
114114
* Fix combined labels with comments being ignored spockPull:2121[]
115115
* Fix boxed Boolean `is` getter methods not properly mocked in Groovy <= 3 spockIssue:2131[]
116+
* Fix `SpyStatic()` with an interaction closure throws NullPointerException spockPull:2254[]
116117

117118
Thanks to all the contributors to this release: Andreas Turban, Björn Kautler, Christoph Loy, Marcin Zajączkowski, Pavlo Shevchenko
118119

spock-core/src/main/java/org/spockframework/mock/runtime/MockMakerRegistry.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@
1616

1717
package org.spockframework.mock.runtime;
1818

19+
import groovy.lang.Closure;
1920
import org.spockframework.mock.CannotCreateMockException;
2021
import org.spockframework.mock.IMockObject;
2122
import org.spockframework.mock.ISpockMockObject;
2223
import org.spockframework.mock.runtime.IMockMaker.IStaticMock;
2324
import org.spockframework.mock.runtime.IMockMaker.MockMakerCapability;
25+
import org.spockframework.runtime.GroovyRuntimeUtil;
2426
import org.spockframework.util.InternalSpockError;
2527
import org.spockframework.util.Nullable;
2628
import org.spockframework.util.ThreadSafe;
2729
import spock.mock.IMockMakerSettings;
2830
import spock.mock.MockMakerId;
2931

3032
import java.lang.reflect.Modifier;
33+
import java.lang.reflect.Proxy;
3134
import java.util.ArrayList;
3235
import java.util.Collections;
3336
import java.util.Comparator;
@@ -77,7 +80,7 @@ public static MockMakerRegistry createFromServiceLoader(MockMakerConfiguration c
7780
MockMakerId preferredMockMakerId;
7881
if (preferredMockMakerParam != null) {
7982
preferredMockMakerId = preferredMockMakerParam.getMockMakerId();
80-
if (makerMap.get(preferredMockMakerParam.getMockMakerId()) == null) {
83+
if (makerMap.get(preferredMockMakerId) == null) {
8184
throw new IllegalStateException("No IMockMaker with ID '" + preferredMockMakerId + "' exists, but was request via mockMaker.preferredMockMaker configuration. Is a runtime dependency missing?");
8285
}
8386
} else {
@@ -142,9 +145,10 @@ public IStaticMock makeStaticMock(IMockCreationSettings settings) throws CannotC
142145
public <T> T makeMockInternal(IMockCreationSettings settings, BiFunction<IMockMaker, IMockCreationSettings, T> code) throws CannotCreateMockException {
143146
IMockMakerSettings mockMakerSettings = settings.getMockMakerSettings();
144147
if (mockMakerSettings != null) {
145-
MockMakerId mockMakerId = mockMakerSettings.getMockMakerId();
148+
MockMakerId mockMakerId = getMockMakerId(settings, mockMakerSettings);
146149
IMockMaker mockMaker = makerMap.get(mockMakerId);
147150
if (mockMaker == null) {
151+
checkForStaticMockUsageWithClosure(settings, mockMakerSettings, mockMakerId, null);
148152
throw new CannotCreateMockException(settings.getMockType(), " because MockMaker with ID '" + mockMakerId + "' does not exist.");
149153
}
150154
verifyIsMockable(mockMaker, settings);
@@ -153,6 +157,25 @@ public <T> T makeMockInternal(IMockCreationSettings settings, BiFunction<IMockMa
153157
return createWithAppropriateMockMaker(settings, code);
154158
}
155159

160+
private static MockMakerId getMockMakerId(IMockCreationSettings settings, IMockMakerSettings mockMakerSettings) {
161+
try {
162+
return mockMakerSettings.getMockMakerId();
163+
} catch (ClassCastException ex) {
164+
checkForStaticMockUsageWithClosure(settings, mockMakerSettings, null, ex);
165+
throw ex;
166+
}
167+
}
168+
169+
private static void checkForStaticMockUsageWithClosure(IMockCreationSettings settings, IMockMakerSettings mockMakerSettings, @Nullable MockMakerId mockMakerId, @Nullable Throwable cause) {
170+
if (settings.isStaticMock() && (mockMakerSettings instanceof Proxy || mockMakerSettings instanceof Closure)) {
171+
String nature = settings.getMockNature().toString();
172+
throw new CannotCreateMockException(settings.getMockType(), " because the MockMakerSettings returned the invalid ID '" + mockMakerId + "'."
173+
+ "\nThe syntax " + nature + "Static(" + settings.getMockType().getSimpleName() + "){} is not supported, please use " + nature + "Static(" + settings.getMockType().getSimpleName() + ") without a Closure instead."
174+
, cause
175+
);
176+
}
177+
}
178+
156179
/**
157180
* Returns information about a mock object, or {@code null} if the object is no mock.
158181
*

spock-core/src/main/java/spock/mock/IMockMakerSettings.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* <p>If you provide a method, you can define your settings in a typesafe manner for the user,
1717
* e.g. with a {@link Closure} parameter to configure the mock.
1818
*
19+
* @author Andreas Turban
1920
* @since 2.4
2021
*/
2122
@Beta
@@ -41,5 +42,8 @@ public String toString() {
4142
};
4243
}
4344

45+
/**
46+
* @return the {@link MockMakerId} to use, must not be {@code null}.
47+
*/
4448
MockMakerId getMockMakerId();
4549
}

spock-specs/src/test/groovy/org/spockframework/mock/runtime/mockito/MockitoStaticMocksSpec.groovy

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ import org.mockito.exceptions.base.MockitoException
2222
import org.spockframework.mock.CannotCreateMockException
2323
import org.spockframework.mock.MockUtil
2424
import org.spockframework.runtime.InvalidSpecException
25+
import org.spockframework.runtime.SpecInternals
26+
import org.spockframework.runtime.model.SpecInfo
2527
import spock.lang.Issue
2628
import spock.lang.Shared
2729
import spock.lang.Specification
30+
import spock.mock.IMockMakerSettings
2831
import spock.mock.MockMakers
2932

3033
import java.util.concurrent.Callable
@@ -491,6 +494,43 @@ class MockitoStaticMocksSpec extends Specification {
491494
!StaticClass.staticVarargsMethod("test2")
492495
}
493496

497+
def "SpyStatic with closure as IMockMakerSettings shall produce nice error message"() {
498+
when:
499+
SpyStatic(StaticClass) {
500+
501+
}
502+
503+
then:
504+
def ex = thrown(CannotCreateMockException)
505+
ex.message.startsWith(
506+
"""Cannot create mock for class $StaticClass.name because the MockMakerSettings returned the invalid ID 'null'.
507+
The syntax SpyStatic(StaticClass){} is not supported, please use SpyStatic(StaticClass) without a Closure instead.""")
508+
}
509+
510+
def "SpyStatic with closure returning something shall produce nice error message"() {
511+
when:
512+
SpyStatic(StaticClass) {
513+
"Dummy"
514+
}
515+
516+
then:
517+
def ex = thrown(CannotCreateMockException)
518+
ex.message.startsWith(
519+
"""Cannot create mock for class $StaticClass.name because the MockMakerSettings returned the invalid ID 'null'.
520+
The syntax SpyStatic(StaticClass){} is not supported, please use SpyStatic(StaticClass) without a Closure instead.""")
521+
ex.cause instanceof ClassCastException
522+
}
523+
524+
def "ClassCastException inside the getMockMakerId() from a non-static Spy is thrown as-is and is not processed by the SpyStatic(){} closure check"() {
525+
when:
526+
Spy(Runnable, mockMaker: {
527+
throw new ClassCastException()
528+
} as IMockMakerSettings)
529+
530+
then:
531+
thrown(ClassCastException)
532+
}
533+
494534
static class StaticClass {
495535
496536
String instanceMethod() {

0 commit comments

Comments
 (0)