Skip to content

Commit 2050e56

Browse files
committed
Fix SpyStatic() with an interaction closure throws NullPointerException
The usage of SpyStaticImpl(Type){ } had thrown an NullPointerException, because the closure got converted into IMockMakerSettings.
1 parent c30d2ab commit 2050e56

File tree

7 files changed

+109
-5
lines changed

7 files changed

+109
-5
lines changed

docs/release_notes.adoc

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

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

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

Lines changed: 7 additions & 0 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;
@@ -145,6 +148,10 @@ public <T> T makeMockInternal(IMockCreationSettings settings, BiFunction<IMockMa
145148
MockMakerId mockMakerId = mockMakerSettings.getMockMakerId();
146149
IMockMaker mockMaker = makerMap.get(mockMakerId);
147150
if (mockMaker == null) {
151+
if (mockMakerId == null && (mockMakerSettings instanceof Proxy || mockMakerSettings instanceof Closure)) {
152+
throw new CannotCreateMockException(settings.getMockType(), " because the MockMakerSettings returned the invalid ID 'null'. Please check that a closure did not get accidentally casted to IMockMakerSettings."
153+
+ " The settings object was '" + GroovyRuntimeUtil.toString(mockMakerSettings) + "'.");
154+
}
148155
throw new CannotCreateMockException(settings.getMockType(), " because MockMaker with ID '" + mockMakerId + "' does not exist.");
149156
}
150157
verifyIsMockable(mockMaker, settings);

spock-core/src/main/java/org/spockframework/runtime/SpecInternals.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,18 @@ private static <T> T createMockImpl(Specification specification, String inferred
333333
}
334334

335335
public static void SpyStaticImpl(Specification specification, String inferredName, Type inferredType, Class<?> specifiedType) {
336-
createStaticMockImpl(specification, MockNature.SPY, specifiedType, null);
336+
createStaticMockImpl(specification, MockNature.SPY, specifiedType, null, null);
337+
}
338+
339+
public static void SpyStaticImpl(Specification specification, String inferredName, Type inferredType, Class<?> specifiedType, Closure<?> closure) {
340+
createStaticMockImpl(specification, MockNature.SPY, specifiedType, null, closure);
337341
}
338342

339343
public static void SpyStaticImpl(Specification specification, String inferredName, Type inferredType, Class<?> specifiedType, IMockMakerSettings mockMakerSettings) {
340-
createStaticMockImpl(specification, MockNature.SPY, specifiedType, mockMakerSettings);
344+
createStaticMockImpl(specification, MockNature.SPY, specifiedType, mockMakerSettings, null);
341345
}
342346

343-
private static void createStaticMockImpl(Specification specification, MockNature nature, Class<?> specifiedType, @Nullable IMockMakerSettings mockMakerSettings) {
347+
private static void createStaticMockImpl(Specification specification, MockNature nature, Class<?> specifiedType, @Nullable IMockMakerSettings mockMakerSettings, @Nullable Closure<?> closure) {
344348
if (specifiedType == null) {
345349
throw new InvalidSpecException("The type must not be null.");
346350
}
@@ -349,5 +353,8 @@ private static void createStaticMockImpl(Specification specification, MockNature
349353
options = Collections.singletonMap("mockMaker", mockMakerSettings);
350354
}
351355
createStaticMock(specification, specifiedType, nature, options);
356+
if (closure != null) {
357+
GroovyRuntimeUtil.invokeClosure(closure, specifiedType);
358+
}
352359
}
353360
}

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-core/src/main/java/spock/mock/MockingApi.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,32 @@ public <T> void SpyStatic(Class<T> type) {
16431643
throw invalidMockCreation();
16441644
}
16451645

1646+
/**
1647+
* Creates a thread-local spy for all static methods of the passed type.
1648+
* <p>
1649+
* Example:
1650+
*
1651+
* <pre>
1652+
* SpyStatic(Person){
1653+
* Person.staticMethod() >> "result"
1654+
* }
1655+
* </pre>
1656+
*
1657+
* <p>Please use the class name as a prefix, instead of relying on {@code it} from Groovy.</p>
1658+
*
1659+
* <p>If you want to activate the static mocks on a different {@code Thread},
1660+
* please call {@link #runWithThreadAwareMocks(Runnable)} on the different {@code Thread}.
1661+
*
1662+
* @param type the type of which the static methods shall be spied
1663+
* @param interactions a description of the static spy's interactions
1664+
*/
1665+
@Beta
1666+
public <T> void SpyStatic(Class<T> type, Closure<?> interactions //Note: We can't specify here @ClosureParams or @DelegatesTo because there is no syntax to express that it means the Class object
1667+
) {
1668+
1669+
throw invalidMockCreation();
1670+
}
1671+
16461672
/**
16471673
* Creates a thread-local spy for all static methods of the passed type.
16481674
* <p>
@@ -1663,7 +1689,6 @@ public <T> void SpyStatic(Class<T> type, IMockMakerSettings mockMakerSettings) {
16631689
throw invalidMockCreation();
16641690
}
16651691

1666-
16671692
/**
16681693
* Runs the code with the thread-aware mocks activated on the current {@link Thread}.
16691694
*

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.spockframework.runtime.InvalidSpecException
2525
import spock.lang.Issue
2626
import spock.lang.Shared
2727
import spock.lang.Specification
28+
import spock.mock.IMockMakerSettings
2829
import spock.mock.MockMakers
2930

3031
import java.util.concurrent.Callable
@@ -491,6 +492,64 @@ class MockitoStaticMocksSpec extends Specification {
491492
!StaticClass.staticVarargsMethod("test2")
492493
}
493494

495+
def "SpyStatic with closure shall not lead an exception"() {
496+
when:
497+
SpyStatic(StaticClass) {
498+
499+
}
500+
501+
then:
502+
noExceptionThrown()
503+
}
504+
505+
def "SpyStatic with closure can specify interactions"() {
506+
given:
507+
SpyStatic(StaticClass) {
508+
StaticClass.staticVarargsMethod("test") >> true
509+
}
510+
511+
expect:
512+
StaticClass.staticVarargsMethod("test")
513+
}
514+
515+
def "SpyStatic with closure could use it instead of ClassName"() {
516+
given:
517+
SpyStatic(StaticClass) {
518+
//Note: This does not have code completion support.
519+
it.staticVarargsMethod("test") >> true
520+
}
521+
522+
expect:
523+
StaticClass.staticVarargsMethod("test")
524+
}
525+
526+
def "SpyStatic with closure could use no prefix instead of ClassName"() {
527+
given:
528+
SpyStatic(StaticClass) {
529+
//Note: This does not have code completion support.
530+
staticVarargsMethod("test") >> true
531+
}
532+
533+
expect:
534+
StaticClass.staticVarargsMethod("test")
535+
}
536+
537+
/**
538+
* The IMockMakerSettings has only a single method, so it could be converted from a Groovy closure automatically.
539+
* This shall check that the API produces a nice error to the user in such cases.
540+
*/
541+
def "SpyStatic with closure casted as IMockMakerSettings shall produce nice error message"() {
542+
when:
543+
SpyStatic(StaticClass, {
544+
545+
} as IMockMakerSettings)
546+
547+
then:
548+
def ex = thrown(CannotCreateMockException)
549+
ex.message.startsWith(
550+
"Cannot create mock for class $StaticClass.name because the MockMakerSettings returned the invalid ID 'null'. Please check that a closure did not get accidentally casted to IMockMakerSettings. The settings object was ")
551+
}
552+
494553
static class StaticClass {
495554

496555
String instanceMethod() {

spock-specs/src/test/groovy/spock/mock/MockingApiInvalidUsageSpec.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class MockingApiInvalidUsageSpec extends Specification {
9595
"GroovySpy" | [CLOSURE]
9696

9797
"SpyStatic" | [Runnable]
98-
"SpyStatic" | [Runnable, MockMakers.mockito]
98+
"SpyStatic" | [Runnable, CLOSURE]
99+
"SpyStatic" | [Runnable, MockMakers.mockito]
99100
}
100101
}

0 commit comments

Comments
 (0)