Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ spockPull:2112[]
* Fix filter blocks with shared fields and derived data variables spockPull:2088[]
* Fix combined labels with comments being ignored spockPull:2121[]
* Fix boxed Boolean `is` getter methods not properly mocked in Groovy <= 3 spockIssue:2131[]
* Fix `SpyStatic()` with an interaction closure throws NullPointerException spockPull:2254[]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this misleading? IMO, this adds a feature that didn't exist before.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does. I am not really convinced that a user should use the new syntax:

SpyStatic(Type){
  1 * Type.method() >> true
}

because it does not work so great, due to the Class instance issues, e.g. IDE support.

The better syntax is the documented one, without any closure.

SpyStatic(Type)
1 * Type.method() >> true

As I said, I can also remove the new syntax and add a second method to IMockMakerSettings to prevent the auto-conversion, if you like that better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leonard84 I have changed PR to not use new API, you can have a look what you like better :).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it again, because the new method to IMockMakerSettings would break contribution API for external MockMakers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why do we not support something like

SpyStatic(Type) {
  1 * method() >> true
}

like we do for all other mock objects?
Imho it should be up to the user whether he does that or

SpyStatic(Type)
1 * Type.method() >> true

like is supported for all other mock objects.
And it should solve the problem, as with a Closure overload, that should be used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leonard sounded that he was not so convinced about that new API, especially the lack of IDE support.
So I made a non-API change bugfix.

Copy link
Member

@Vampire Vampire Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just treat it like an instance is given in?
Yes, you then get also instance methods completed, but as you can also call static methods on instances, you also get the static methods.
On the other mock closures you also get both while calling the static does not really make sense.
So I'd say just add the @ClosureParams and @DelegatesTo annotations and add an ?: enclosingCall('SpyStatic') to the gdsl and whatever is necessary for the Eclipse-version and it should be fine enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't you get IDE warnings that static methods are called on an instance?

Actually before this, I would perfer the nothing at all as in my commit and the user shall use the class syntax inside the closure, which works fine and completes correctly.

SpyStatic(Type){
  1 * Type.method() >> true
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't you get IDE warnings that static methods are called on an instance?

I don't think the IDE can warn about this, because due to the duck-typing of Groovy, the IDE cannot know whether the instance might also have an instance method with that name at runtime. If it complains, I'd say that is an IDE bug.

But actually I do not see a warning even with

@DelegatesTo(strategy = Closure.DELEGATE_FIRST, genericTypeIndex = 0)
@ClosureParams(FirstParam.FirstGenericType.class)

But you can also do

@DelegatesTo(strategy = Closure.DELEGATE_FIRST)
@ClosureParams(FirstParam.class)

and then the type is correctly considered to be Class<StaticClass> and the static methods are completed as expected too.

And the ?: enclosingCall('StaticSpy') in the spock.gdsl should also work as expected, as it should also deliver Class<StaticClass> as type and thus be correct.

For the spock_tests.dsld I'd expect the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my comment was more about the Fix part. The way it is written, you fixed something that didn't exist before, so the more appropriate line would be Add <feature>.
Now with the feature removed, the Fix prefix makes sense again.

To be clear, I'm not fundamentally opposed to an initializing closure.
I'm going to merge this as-is to hopefully release M7 today.


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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@

package org.spockframework.mock.runtime;

import groovy.lang.Closure;
import org.spockframework.mock.CannotCreateMockException;
import org.spockframework.mock.IMockObject;
import org.spockframework.mock.ISpockMockObject;
import org.spockframework.mock.runtime.IMockMaker.IStaticMock;
import org.spockframework.mock.runtime.IMockMaker.MockMakerCapability;
import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.util.InternalSpockError;
import org.spockframework.util.Nullable;
import org.spockframework.util.ThreadSafe;
import spock.mock.IMockMakerSettings;
import spock.mock.MockMakerId;

import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -77,7 +80,7 @@ public static MockMakerRegistry createFromServiceLoader(MockMakerConfiguration c
MockMakerId preferredMockMakerId;
if (preferredMockMakerParam != null) {
preferredMockMakerId = preferredMockMakerParam.getMockMakerId();
if (makerMap.get(preferredMockMakerParam.getMockMakerId()) == null) {
if (makerMap.get(preferredMockMakerId) == null) {
throw new IllegalStateException("No IMockMaker with ID '" + preferredMockMakerId + "' exists, but was request via mockMaker.preferredMockMaker configuration. Is a runtime dependency missing?");
}
} else {
Expand Down Expand Up @@ -142,9 +145,10 @@ public IStaticMock makeStaticMock(IMockCreationSettings settings) throws CannotC
public <T> T makeMockInternal(IMockCreationSettings settings, BiFunction<IMockMaker, IMockCreationSettings, T> code) throws CannotCreateMockException {
IMockMakerSettings mockMakerSettings = settings.getMockMakerSettings();
if (mockMakerSettings != null) {
MockMakerId mockMakerId = mockMakerSettings.getMockMakerId();
MockMakerId mockMakerId = getMockMakerId(settings, mockMakerSettings);
IMockMaker mockMaker = makerMap.get(mockMakerId);
if (mockMaker == null) {
checkForStaticMockUsageWithClosure(settings, mockMakerSettings, mockMakerId, null);
throw new CannotCreateMockException(settings.getMockType(), " because MockMaker with ID '" + mockMakerId + "' does not exist.");
}
verifyIsMockable(mockMaker, settings);
Expand All @@ -153,6 +157,25 @@ public <T> T makeMockInternal(IMockCreationSettings settings, BiFunction<IMockMa
return createWithAppropriateMockMaker(settings, code);
}

private static MockMakerId getMockMakerId(IMockCreationSettings settings, IMockMakerSettings mockMakerSettings) {
try {
return mockMakerSettings.getMockMakerId();
} catch (ClassCastException ex) {
checkForStaticMockUsageWithClosure(settings, mockMakerSettings, null, ex);
throw ex;
}
}

private static void checkForStaticMockUsageWithClosure(IMockCreationSettings settings, IMockMakerSettings mockMakerSettings, @Nullable MockMakerId mockMakerId, @Nullable Throwable cause) {
if (settings.isStaticMock() && (mockMakerSettings instanceof Proxy || mockMakerSettings instanceof Closure)) {
String nature = settings.getMockNature().toString();
throw new CannotCreateMockException(settings.getMockType(), " because the MockMakerSettings returned the invalid ID '" + mockMakerId + "'."
+ "\nThe syntax " + nature + "Static(" + settings.getMockType().getSimpleName() + "){} is not supported, please use " + nature + "Static(" + settings.getMockType().getSimpleName() + ") without a Closure instead."
, cause
);
}
}

/**
* Returns information about a mock object, or {@code null} if the object is no mock.
*
Expand Down
4 changes: 4 additions & 0 deletions spock-core/src/main/java/spock/mock/IMockMakerSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* <p>If you provide a method, you can define your settings in a typesafe manner for the user,
* e.g. with a {@link Closure} parameter to configure the mock.
*
* @author Andreas Turban
* @since 2.4
*/
@Beta
Expand All @@ -41,5 +42,8 @@ public String toString() {
};
}

/**
* @return the {@link MockMakerId} to use, must not be {@code null}.
*/
MockMakerId getMockMakerId();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import org.mockito.exceptions.base.MockitoException
import org.spockframework.mock.CannotCreateMockException
import org.spockframework.mock.MockUtil
import org.spockframework.runtime.InvalidSpecException
import org.spockframework.runtime.SpecInternals
import org.spockframework.runtime.model.SpecInfo
import spock.lang.Issue
import spock.lang.Shared
import spock.lang.Specification
import spock.mock.IMockMakerSettings
import spock.mock.MockMakers

import java.util.concurrent.Callable
Expand Down Expand Up @@ -491,6 +494,43 @@ class MockitoStaticMocksSpec extends Specification {
!StaticClass.staticVarargsMethod("test2")
}

def "SpyStatic with closure as IMockMakerSettings shall produce nice error message"() {
when:
SpyStatic(StaticClass) {

}

then:
def ex = thrown(CannotCreateMockException)
ex.message.startsWith(
"""Cannot create mock for class $StaticClass.name because the MockMakerSettings returned the invalid ID 'null'.
The syntax SpyStatic(StaticClass){} is not supported, please use SpyStatic(StaticClass) without a Closure instead.""")
}

def "SpyStatic with closure returning something shall produce nice error message"() {
when:
SpyStatic(StaticClass) {
"Dummy"
}

then:
def ex = thrown(CannotCreateMockException)
ex.message.startsWith(
"""Cannot create mock for class $StaticClass.name because the MockMakerSettings returned the invalid ID 'null'.
The syntax SpyStatic(StaticClass){} is not supported, please use SpyStatic(StaticClass) without a Closure instead.""")
ex.cause instanceof ClassCastException
}

def "ClassCastException inside the getMockMakerId() from a non-static Spy is thrown as-is and is not processed by the SpyStatic(){} closure check"() {
when:
Spy(Runnable, mockMaker: {
throw new ClassCastException()
} as IMockMakerSettings)

then:
thrown(ClassCastException)
}

static class StaticClass {

String instanceMethod() {
Expand Down