Skip to content

Commit

Permalink
Add IDefaultValueProviderExtension (#1994)
Browse files Browse the repository at this point in the history
to make EmptyOrDummyResponse extensible for types that need special
handling.
  • Loading branch information
leonard84 authored Sep 7, 2024
1 parent 7c7d010 commit ef8e05d
Show file tree
Hide file tree
Showing 15 changed files with 435 additions and 1 deletion.
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ subprojects {
groovy {
srcDir "src/${ss.name}-groovy-le-$gv/groovy"
}
resources {
srcDir "src/${ss.name}-groovy-le-$gv/resources"
}
}
for (jv in javaVersions.findAll { javaVersion <= it }) {
java {
Expand All @@ -137,6 +140,9 @@ subprojects {
groovy {
srcDir "src/${ss.name}-java-le-$jv/groovy"
}
resources {
srcDir "src/${ss.name}-java-le-$jv/resources"
}
}
for (gv in variants.findAll { variant >= it }) {
java {
Expand All @@ -145,6 +151,9 @@ subprojects {
groovy {
srcDir "src/${ss.name}-groovy-ge-$gv/groovy"
}
resources {
srcDir "src/${ss.name}-groovy-ge-$gv/resources"
}
}
for (jv in javaVersions.findAll { javaVersion >= it }) {
java {
Expand All @@ -153,6 +162,9 @@ subprojects {
groovy {
srcDir "src/${ss.name}-java-ge-$jv/groovy"
}
resources {
srcDir "src/${ss.name}-java-ge-$jv/resources"
}
}
}

Expand Down
23 changes: 23 additions & 0 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1354,3 +1354,26 @@ The extension generates the following output for the example code above.
----
include::{sourcedir}/extension/ExtensionStoreSpec.groovy[tag=example-result]
----


[[default-value-provider]]
=== Default Value Provider Extensions

Stubs use the `EmptyOrDummyResponse` to return a default value for a method call.
If the `EmptyOrDummyResponse` doesn't have specific instructions for a type it will return a new `Stub` for that type.
This works for most cases, but can lead to unexpected behavior if the type does have a behavioral contract that is not fulfilled by the `Stub`.
Another problematic case is when the type is `final` or `sealed` as those are not mockable by default.

To address these issues, Spock 2.4 introduces the `org.spockframework.runtime.extension.IDefaultValueProviderExtension` that is loaded via Java's `ServiceLoader` mechanism.
This extension allows you to provide a default value if the `EmptyOrDummyResponse` doesn't have specific instructions for a type.

.Example Implementation
[source,groovy,indent=0]
----
include::{sourcedir-java}/smoke/mock/MaybeDefaultValueProvider.java[tag=sample-implementation]
----

It is primarily for framework developers who want to provide a default value for their framework types.
Or users of a framework that doesn't provide default values for their special types.

If you want to change the default response behavior for `Stub` have a look at <<interaction_based_testing.adoc#ALaCarteMocks,A la Carte Mocks>> and how to use your own `org.spockframework.mock.IDefaultResponse`.
4 changes: 4 additions & 0 deletions docs/include.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
:github-base: https://github.com/spockframework/spock
:github-blob-base: {github-base}/blob
:commit-ish: master
// source java dir
:base-sourcedir-java: spock-specs/src/test/java/org/spockframework
:sourcedir-java: ../{base-sourcedir-java}
:github-sourcedir-java: {github-blob-base}/{commit-ish}/{base-sourcedir-java}
// source dir
:base-sourcedir: spock-specs/src/test/groovy/org/spockframework/docs
:sourcedir: ../{base-sourcedir}
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ include::include.adoc[]
=== Misc

* Add `globalTimeout` to `@Timeout` extension, to apply a timeout to all features in a specification, configurable via the spock configuration file spockPull:1986[]
* Add new <<extensions.adoc#default-value-provider,`IDefaultValueProviderExtension`>> extension point to add support for special classes in the Stub's default `EmptyOrDummyResponse` spockPull:1994[]
* Improve `@Timeout` extension will now use virtual threads if available spockPull:1986[]
* Improve mock argument matching, types constraints or arguments in interactions can now handle primitive types like `_ as int` spockIssue:1974[]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.spockframework.mock;

import org.spockframework.runtime.extension.IDefaultValueProviderExtension;
import org.spockframework.util.ReflectionUtil;
import org.spockframework.util.ThreadSafe;
import spock.lang.Specification;
Expand All @@ -35,8 +36,17 @@
@ThreadSafe
public class EmptyOrDummyResponse implements IDefaultResponse {
public static final EmptyOrDummyResponse INSTANCE = new EmptyOrDummyResponse();
private final List<IDefaultValueProviderExtension> defaultValueProviders;

private EmptyOrDummyResponse() {}
private EmptyOrDummyResponse() {
ServiceLoader<IDefaultValueProviderExtension> serviceLoader = ServiceLoader.load(IDefaultValueProviderExtension.class);
List<IDefaultValueProviderExtension> providers = new ArrayList<>();
for (IDefaultValueProviderExtension provider : serviceLoader) {
providers.add(provider);
}
providers.sort(Comparator.comparing(p -> p.getClass().getName()));
defaultValueProviders = Collections.unmodifiableList(providers);
}

@Override
@SuppressWarnings("rawtypes")
Expand Down Expand Up @@ -72,6 +82,8 @@ public Object respond(IMockInvocation invocation) {
if (returnType == IntStream.class) return IntStream.empty();
if (returnType == DoubleStream.class) return DoubleStream.empty();
if (returnType == LongStream.class) return LongStream.empty();
Object providedValue = askDefaultValueProviders(invocation);
if (providedValue != null) return providedValue;
return createDummy(invocation);
}

Expand All @@ -98,6 +110,9 @@ public Object respond(IMockInvocation invocation) {
Object emptyWrapper = createEmptyWrapper(returnType);
if (emptyWrapper != null) return emptyWrapper;

Object providedValue = askDefaultValueProviders(invocation);
if (providedValue != null) return providedValue;

Object emptyObject = createEmptyObject(returnType);
if (emptyObject != null) return emptyObject;

Expand Down Expand Up @@ -128,6 +143,19 @@ private Object createEmptyObject(Class<?> type) {
}
}

private Object askDefaultValueProviders(IMockInvocation invocation) {
if (defaultValueProviders.isEmpty()) {
return null;
}
Class<?> returnType = invocation.getMethod().getReturnType();
Type exactReturnType = invocation.getMethod().getExactReturnType();
return defaultValueProviders.stream()
.map(provider -> provider.provideDefaultValue(returnType, exactReturnType))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

private Object createDummy(IMockInvocation invocation) {
Class<?> type = invocation.getMethod().getReturnType();
Type genericType = invocation.getMethod().getExactReturnType();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.runtime.extension;

import java.lang.reflect.Type;

import org.spockframework.mock.IMockMethod;
import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;
import org.spockframework.util.ThreadSafe;

/**
* Allows to enhance {@link org.spockframework.mock.EmptyOrDummyResponse} with custom default values.
* <p>
* Will be instantiated via the {@link java.util.ServiceLoader} mechanism.
* <p>
* Implementations must be thread-safe and should not have any state.
* <p>
* This extension is intended for framework authors and users who want to provide default values for their own types.
* The extension will only be called if no sensible default value can be provided by the default mechanism.
* If you want to change the default behavior for all types, you should implement a custom {@link org.spockframework.mock.IDefaultResponse}.
*
* @since 2.4
* @see org.spockframework.mock.EmptyOrDummyResponse
* @see org.spockframework.mock.IDefaultResponse
*/
@Beta
@ThreadSafe
public interface IDefaultValueProviderExtension {
/**
* Provides a default value for the given type.
* <p>
* This method will be called for every `EmptyOrDummyResponse` non-default type, the returned values are not cached.
*
* @param type the type for which a default value should be provided, see {@link IMockMethod#getReturnType()}
* @param exactType the exact type for which a default value should be provided, see {@link IMockMethod#getExactReturnType()}
* @return the value or null if no default value can be provided
*/
@Nullable
Object provideDefaultValue(Class<?> type, Type exactType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.smoke.mock;

public sealed interface IEither<L, R> {
record Left<L, R>(L value) implements IEither<L, R> {
}

record Right<L, R>(R value) implements IEither<L, R> {
}

static <L, R> IEither<L, R> left(L value) {
return new Left<>(value);
}

static <L, R> IEither<L, R> right(R value) {
return new Right<>(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.smoke.mock;

public sealed interface IMaybe<T> {

record Some<T>(T value) implements IMaybe<T> {
}

record None<T>() implements IMaybe<T> {
}

static <T> IMaybe<T> some(T value) {
return new Some<>(value);
}

static <T> IMaybe<T> none() {
return new None<>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.smoke.mock;

public interface IEither<L, R> {
class Left<L, R> implements IEither<L, R> {
private final L value;

public Left(L value) {
this.value = value;
}

public L value() {
return value;
}
}

class Right<L, R> implements IEither<L, R> {
private final R value;

public Right(R value) {
this.value = value;
}

public R value() {
return value;
}
}

static <L, R> IEither<L, R> left(L value) {
return new Left<>(value);
}

static <L, R> IEither<L, R> right(R value) {
return new Right<>(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.spockframework.smoke.mock;

public interface IMaybe<T> {

class Some<T> implements IMaybe<T> {
private final T value;

Some(T value) {
this.value = value;
}

public T getValue() {
return value;
}
}

class None<T> implements IMaybe<T> {
}

static <T> IMaybe<T> some(T value) {
return new Some<>(value);
}

static <T> IMaybe<T> none() {
return new None<>();
}
}
Loading

0 comments on commit ef8e05d

Please sign in to comment.