Skip to content

Commit

Permalink
MockitoMockMaker provides mocking of static methods
Browse files Browse the repository at this point in the history
MockitoMockMaker supports to mock static methods
of Java classes with the new Spec API:

`MockStatic()`
`StubStatic()`
`SpyStatic()`

This also supports Java callers of the static methods.

Added MockUtil.isStaticMock()

Added code to use public Mockito API introduced with
mockito/mockito#3129

Moved IMockMakerSettings and MockMakerId to package spock.mock.

Co-authored-by: Leonard Brünings <lord_damokles@gmx.net>
  • Loading branch information
AndreasTu and leonard84 committed Nov 12, 2023
1 parent 9feb9dd commit 3abddfe
Show file tree
Hide file tree
Showing 41 changed files with 2,125 additions and 322 deletions.
7 changes: 6 additions & 1 deletion docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ The following mock makers are built-in, and are selected in this order:
| Explicit Constructor Arguments | ✘ | ✔ | ✔ | ✔
| Final Class | ✘ | ✘ | ✘ | ✔
| Final Method | ✘ | ✘ | ✘ | ✔
| Static Method | ✘ | ✘ | ✘ |
| Static Method | ✘ | ✘ | ✘ |
|===

The class `spock.mock.MockMakers` provides constants and methods for the built-in mock makers.
Expand All @@ -1102,6 +1102,7 @@ The preferred mock maker will be used globally, if no mock maker is explicitly s
include::{sourcedir}/extension/MockMakerConfigurationDocSpec.groovy[tag=mock-maker-preferredMockMaker]
----

[[mock-makers-mockito]]
==== Mockito Mock Maker

The `mockito` Mock Maker provides the ability to mock final types, enums and final methods.
Expand Down Expand Up @@ -1131,6 +1132,10 @@ The `mockito` mock maker uses `org.mockito.MockMakers.INLINE` under the hood,
please see the Mockito manual "Mocking final types, enums and final methods" for all pros and cons,
when using `org.mockito.MockMakers.INLINE`.


The `mockito` mock maker also supports mocking of static methods of classes and interfaces with `Mock/Stub/SpyStatic`.
See <<interaction_based_testing.adoc#static-mocks,static mocks>> section for more details.

==== Custom Mock Maker

Spock provides an extension point to plug in your own mock maker for creating mock objects.
Expand Down
81 changes: 80 additions & 1 deletion docs/interaction_based_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,84 @@ a subscriber named Barney instead.
[MockingStaticMethods]
=== Mocking Static Methods

(Think twice before using this feature. It might be better to change the design of the code under specification.)
Spock supports two ways to mock static methods:

* `Mock/Stub/SpyStatic` static mocks: Works with Java and Groovy, but requires a mock maker supporting this, e.g. `mockito`.
* Global Groovy mocks: Work only for Groovy code not for Java. If parallel execution is enabled, then the test must be annotated with `@ResourceLock(Resources.META_CLASS_REGISTRY)` or `@Isolated`.

NOTE: Think twice before using this feature.
It might be better to change the design of the code under specification.

[[static-mocks]]
==== Static Mocks

You can create static mocks with `MockStatic()`, `StubStatic()` or `SpyStatic()`.
The semantics are the same as for the non-static variants:

* `MockStatic()`: Mocks static methods of the given type that supports both stubbing and mocking.
* `StubStatic()`: Mocks static methods of the given type that supports stubbing but not mocking.
* `SpyStatic()`: Mocks static methods of the given type that, by default, delegates all calls to the real static methods.
Supports both stubbing and mocking.

We are using the class `StaticClass` in the examples:

.Example static class used in the test examples
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/StaticMocksDocSpec.groovy[tag=mock-static-class]
----

We want to mock the method `staticMethod()`, so we can create a static mock for the `StaticClass` type:

.Mock static method of a class
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/StaticMocksDocSpec.groovy[tag=mock-static1]
----

You can also specify the answers during construction:

.Mock static method of a class with answers during construction
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/StaticMocksDocSpec.groovy[tag=mock-static-answers]
----

Or by defining the interactions as for a normal mock:

.Mock static method of a class with interactions
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/StaticMocksDocSpec.groovy[tag=mock-static-interactions]
----

When using `SpyStatic()` all calls will be delegated to the real methods by default:

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/StaticMocksDocSpec.groovy[tag=spy-static]
----

NOTE: The static mocks require a mock maker supporting static methods, e.g. <<extensions.adoc#mock-makers-mockito,mockito>>.
See <<extensions.adoc#mock-makers,mock makers>> table for mock makers supporting it.

===== Static Mocks and Threading

The static mocks are thread-local, so they do not interfere with concurrent test execution.
But this also means that a static mock will not be active, if your code under test will use other threads.

A static mock is activated on the thread, which created the mock, up until the feature execution ends.
You can activate all static mocks on a different thread by hand:

.Use static mocks in a different Thread
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/StaticMocksDocSpec.groovy[tag=mock-static-different-thread]
----

The interface `spock.mock.IStaticMock` provides API to activate a static mock on other threads.

==== Global Groovy Mocks for Static Methods

Global mocks support mocking and stubbing of static methods:

Expand All @@ -1040,6 +1117,8 @@ the mock's instance isn't really needed. In such a case one can just write:
GroovySpy(RealSubscriber, global: true)
----

NOTE: The global mocks will only work for Groovy code under test not for Java code.

== Advanced Features

Most of the time you shouldn't need these features. But if you do, you'll be glad to have them.
Expand Down
21 changes: 21 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ include::include.adoc[]

== 2.4 (tbd)

* Spock now supports pluggable <<extensions.adoc#mock-makers,mock makers>> loaded via ServiceLoader spockPull:1746[]
** This allows external libraries to contribute mocking logic to Spock and use the same API for the users
** You can select the used mock maker during mock creation: `Mock(mockMaker:MockMakers.byteBuddy)`
* Added <<extensions.adoc#mock-makers-mockito,mockito>> mock maker spockPull:1753[] which supports:
** Mocking of final classes and final methods
** Mocking of static methods
** Requires `org.mockito:mockito-core` >= 4.11 in the test class path
* Added support for mocking of static methods also for Java code with the new API `MockStatic/StubStatic/SpyStatic` spockPull:1756[]
** The <<interaction-based-testing.adoc#static-mocks,static mock methods>> will delegate the creation to the mock makers
* Fix issue with mocks of Groovy classes, where the Groovy MOP for `@Internal` methods was not honored by the `byte-buddy` mock maker spockPull:1729[]
** This fixes multiple issues with Groovy MOP: spockIssue:1501[], spockIssue:1452[], spockIssue:1608[] and spockIssue:1145[]
* Reduce lock contention of the `byte-buddy` mock maker, when multiple mocks are created concurrently spockPull:1778[]
* Replaced `gentyref` code with https://github.com/leangen/geantyref[geantyref] library spockPull:1743[]
** This is now a required dependency used by spock: `io.leangen.geantyref:geantyref:1.3.14`
* Better support for generic return types for mocks spockPull:1731[]
** This fixes the issues: spockIssue:520[], spockIssue:1163[]
* Document `@ConditionBlock` Annotation spockPull:1709[]
* Document `old`-Method spockPull:1708[]
* Spock-Compiler does not use wrapper types anymore spockPull:1765[]
* Clarify documentation for global Mocks spockPull:1755[]

=== Breaking Changes

- Calling `old(...)` with multiple arguments is now a compilation error. Previously the additional arguments were simply ignored.
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jaxb = "javax.xml.bind:jaxb-api:2.3.1"
junit4 = "junit:junit:4.13.2"
log4j = "log4j:log4j:1.2.17"
mockito4 = { module = "org.mockito:mockito-core", version.ref = "mockito4" }
mockito4inline = { module = "org.mockito:mockito-inline", version.ref = "mockito4" }
mockito5 = { module = "org.mockito:mockito-core", version.ref = "mockito5" }
objenesis = "org.objenesis:objenesis:3.3"
# This needs a classifier, but is has to be specified on the usage end https://melix.github.io/blog/2021/03/version-catalogs-faq.html#_why_can_t_i_use_excludes_or_classifiers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.spockframework.lang;

import org.spockframework.mock.IStaticMockController;
import org.spockframework.runtime.model.FeatureInfo;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.util.Beta;
Expand All @@ -31,4 +32,6 @@ public interface ISpecificationContext {
Throwable getThrownException();

IMockController getMockController();

IStaticMockController getStaticMockController();
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ public <T> T createMock(@Nullable String name, T instance, Type type, MockNature
return uncheckedCast(mock);
}

private void createStaticMock(Type type, MockNature nature,
Map<String, Object> options,
@Nullable Closure<?> closure) {
MockConfiguration configuration = new MockConfiguration(null, type, null, nature, MockImplementation.JAVA, options);
JavaMockFactory.INSTANCE.createStaticMock(configuration, (Specification) this);
if (closure != null) {
GroovyRuntimeUtil.invokeClosure(closure, type);
}
}

<T> T oldImpl(T expression) {
return expression;
}
Expand Down Expand Up @@ -308,4 +318,65 @@ private <T> T createMockImpl(String inferredName, Class<?> inferredType, T insta
}
return createMock(inferredName, instance, effectiveType, nature, implementation, options, closure);
}

void MockStaticImpl(String inferredName, Class<?> inferredType, Class<?> specifiedType) {
createStaticMockImpl(MockNature.MOCK, specifiedType, null);
}

void MockStaticImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType) {
createStaticMockImpl(MockNature.MOCK, options, specifiedType, null);
}

void MockStaticImpl(String inferredName, Class<?> inferredType, Class<?> specifiedType, Closure<?> closure) {
createStaticMockImpl(MockNature.MOCK, specifiedType, closure);
}

void MockStaticImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType, Closure<?> closure) {
createStaticMockImpl(MockNature.MOCK, options, specifiedType, closure);
}

void StubStaticImpl(String inferredName, Class<?> inferredType, Class<?> specifiedType) {
createStaticMockImpl(MockNature.STUB, specifiedType, null);
}

void StubStaticImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType) {
createStaticMockImpl(MockNature.STUB, options, specifiedType, null);
}

void StubStaticImpl(String inferredName, Class<?> inferredType, Class<?> specifiedType, Closure<?> closure) {
createStaticMockImpl(MockNature.STUB, specifiedType, closure);
}

void StubStaticImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType, Closure<?> closure) {
createStaticMockImpl(MockNature.STUB, options, specifiedType, closure);
}

void SpyStaticImpl(String inferredName, Class<?> inferredType, Class<?> specifiedType) {
createStaticMockImpl(MockNature.SPY, specifiedType, null);
}

void SpyStaticImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType) {
createStaticMockImpl(MockNature.SPY, options, specifiedType, null);
}

void SpyStaticImpl(String inferredName, Class<?> inferredType, Class<?> specifiedType, Closure<?> closure) {
createStaticMockImpl(MockNature.SPY, specifiedType, closure);
}

void SpyStaticImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType, Closure<?> closure) {
createStaticMockImpl(MockNature.SPY, options, specifiedType, closure);
}

private void createStaticMockImpl(MockNature nature, Class<?> specifiedType, @Nullable Closure<?> closure) {
createStaticMockImpl(nature, emptyMap(), specifiedType, closure);
}

private void createStaticMockImpl(MockNature nature, Map<String, Object> options, Class<?> specifiedType, @Nullable Closure<?> closure) {
Type effectiveType = specifiedType != null ? specifiedType : options.containsKey("type") ? (Type) options.get("type") : null;
if (effectiveType == null) {
throw new InvalidSpecException("Mock object type cannot be inferred. " +
"Please specify a type explicitly (e.g. 'MockStatic(Person)').");
}
createStaticMock(effectiveType, nature, options, closure);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.spockframework.mock;

import org.spockframework.mock.runtime.IMockMaker;
import spock.mock.IMockMakerSettings;
import org.spockframework.util.*;
import spock.mock.MockingApi;

Expand Down Expand Up @@ -95,7 +96,7 @@ public interface IMockConfiguration {
* @return the custom settings to use for the creation of the mock, or {@code null}
*/
@Nullable
IMockMaker.IMockMakerSettings getMockMaker();
IMockMakerSettings getMockMaker();

/**
* Tells whether a mock object stands in for all objects of the mocked type, or just for itself.
Expand All @@ -106,7 +107,7 @@ public interface IMockConfiguration {
boolean isGlobal();

/**
* Tells whether invocations on the mock object should be verified. If (@code false}, invocations
* Tells whether invocations on the mock object should be verified. If {@code false}, invocations
* on the mock object will not be matched against interactions that have a cardinality.
*
* @return whether invocations on the mock object should be verified
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2023 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.mock;

import org.spockframework.mock.runtime.IMockMaker;
import org.spockframework.util.Beta;

import java.util.concurrent.Callable;

/**
* The {@code IStaticMockController} provides API to activate a static mocks on non-test {@link Thread Threads}.
*
* @since 2.4
*/
@Beta
public interface IStaticMockController {
/**
* Runs the code with the static mocks activated on the current {@link Thread}.
*
* <p>Note: You only need this if your current {@code Thread} is not the test thread.
* On the test {@code Thread}, the static mocks is automatically activated.</p>
*
* @param code the code to execute
*/
void runWithActiveStaticMocks(Runnable code);

/**
* Runs the code with the static mocks activated on the current {@link Thread}.
*
* <p>Note: You only need this if your current {@code Thread} is not the test thread.
* On the test {@code Thread}, the static mocks is automatically activated.</p>
*
* @param <R> the return type
* @param code the code to execute
* @return the return value of the executed code
*/
<R> R withActiveStaticMocks(Callable<R> code);

void registerStaticMock(IMockMaker.IStaticMock staticMock);
}
10 changes: 10 additions & 0 deletions spock-core/src/main/java/org/spockframework/mock/MockUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ public boolean isMock(Object object) {
return getMockMakerRegistry().asMockOrNull(object) != null;
}

/**
* Returns {@code true} if the passed class is a Spock static mock currently active on the current {@link Thread}.
*
* @param clazz the class to check
* @return {@code true} if this class is a Spock static mock currently active on the current {@code Thread}
*/
public boolean isStaticMock(Class<?> clazz) {
return getMockMakerRegistry().isStaticMock(clazz);
}

private static MockMakerRegistry getMockMakerRegistry() {
return RunContext.get().getMockMakerRegistry();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.spockframework.util.Nullable;
import org.spockframework.util.ReflectionUtil;
import org.spockframework.util.ThreadSafe;
import spock.mock.MockMakerId;

import java.util.Collections;
import java.util.EnumSet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.spockframework.mock.CannotCreateMockException;
import org.spockframework.util.ReflectionUtil;
import org.spockframework.util.ThreadSafe;
import spock.mock.MockMakerId;
import spock.util.environment.Jvm;

import java.util.Collections;
Expand Down

0 comments on commit 3abddfe

Please sign in to comment.