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

Co-authored-by: Leonard Brünings <lord_damokles@gmx.net>
  • Loading branch information
AndreasTu and leonard84 committed Jan 24, 2024
1 parent dd798eb commit 07a4d24
Show file tree
Hide file tree
Showing 22 changed files with 1,761 additions and 135 deletions.
6 changes: 5 additions & 1 deletion docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,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 Down Expand Up @@ -1138,6 +1138,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 @@ -1030,7 +1030,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 @@ -1051,6 +1128,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
3 changes: 3 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ include::include.adoc[]
** 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
* 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[]
* 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[]
* 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

=== Breaking Changes

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
@@ -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 @@ -129,6 +129,16 @@ default IMockObject asMockOrNull(Object object) {
return 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}
*/
default boolean isStaticMock(Class<?> clazz) {
return false;
}

@Immutable
@FunctionalInterface
@Beta
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.spockframework.mock.runtime;

import groovy.lang.GroovyObject;
import org.spockframework.lang.ISpecificationContext;
import org.spockframework.mock.*;
import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.runtime.RunContext;
Expand Down Expand Up @@ -48,14 +49,11 @@ public Object createDetached(IMockConfiguration configuration, ClassLoader class

private Object createInternal(IMockConfiguration configuration, Specification specification, ClassLoader classLoader) {
Class<?> type = configuration.getType();
if (configuration.isGlobal()) {
throw new CannotCreateMockException(type,
" because Java mocks cannot mock globally. If the code under test is written in Groovy, use a Groovy mock.");
}
checkNotGlobal(configuration);

MetaClass mockMetaClass = GroovyRuntimeUtil.getMetaClass(type);
JavaMockInterceptor interceptor = new JavaMockInterceptor(configuration, specification, mockMetaClass);
Object proxy = RunContext.get().getMockMakerRegistry().makeMock(MockCreationSettings.settingsFromMockConfiguration(configuration, interceptor, classLoader));
Object proxy = getMockMakerRegistry().makeMock(MockCreationSettings.settingsFromMockConfiguration(configuration, interceptor, classLoader));
List<Class<?>> additionalInterfaces = configuration.getAdditionalInterfaces();
if (!additionalInterfaces.isEmpty() && GroovyObject.class.isAssignableFrom(type)) {
//Issue #1405: We need to update the mockMetaClass to reflect the methods of the additional interfaces
Expand All @@ -74,4 +72,28 @@ private Object createInternal(IMockConfiguration configuration, Specification sp
}
return proxy;
}

private MockMakerRegistry getMockMakerRegistry() {
return RunContext.get().getMockMakerRegistry();
}

private static void checkNotGlobal(IMockConfiguration configuration) {
if (configuration.isGlobal()) {
throw new CannotCreateMockException(configuration.getType(),
" because Java mocks cannot mock globally. If the code under test is written in Groovy, use a Groovy mock.");
}
}

public void createStaticMock(MockConfiguration configuration, Specification specification) {
checkNotGlobal(configuration);
MetaClass mockMetaClass = GroovyRuntimeUtil.getMetaClass(configuration.getType());
IProxyBasedMockInterceptor interceptor = new JavaMockInterceptor(configuration, specification, mockMetaClass);

MockCreationSettings creationSettings = MockCreationSettings.settingsFromMockConfigurationForStaticMock(configuration, interceptor, specification.getClass().getClassLoader());
IMockMaker.IStaticMock mockMakerStaticMock = getMockMakerRegistry().makeStaticMock(creationSettings);
mockMakerStaticMock.enable();
ISpecificationContext specificationContext = specification.getSpecificationContext();
specificationContext.getCurrentIteration().addCleanup(mockMakerStaticMock::disable);
specificationContext.getStaticMockController().registerStaticMock(mockMakerStaticMock);
}
}

0 comments on commit 07a4d24

Please sign in to comment.