Skip to content

Commit

Permalink
MockitoInlineMockMaker provides mocking of static methods
Browse files Browse the repository at this point in the history
MockitoInlineMockMaker 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
  • Loading branch information
AndreasTu committed Oct 11, 2023
1 parent 9b1e644 commit ef30dfe
Show file tree
Hide file tree
Showing 29 changed files with 1,901 additions and 150 deletions.
6 changes: 5 additions & 1 deletion docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,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 @@ -1092,6 +1092,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 @@ -1121,6 +1122,9 @@ The `mockito` uses the `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`.

==== 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/Sub/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. The test code must run with a `ResourceLock` or single threaded.

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. `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[]

== 2.4-M1 (2022-11-30)

* Fix issues with Spring 6/Spring Boot 3 spockPull:1541[]
Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ groovy3 = '3.0.19'
groovy4 = '4.0.15'
jacoco = '0.8.10'
asm = '9.6'
mockito4 = '4.11.0'

[libraries]
jetbrains-annotations = "org.jetbrains:annotations:24.0.1"
Expand All @@ -17,7 +18,8 @@ hamcrest = "org.hamcrest:hamcrest:2.2"
jaxb = "javax.xml.bind:jaxb-api:2.3.1"
junit4 = "junit:junit:4.13.2"
log4j = "log4j:log4j:1.2.17"
mockito4 = "org.mockito:mockito-core:4.11.0"
mockito4 = { module = "org.mockito:mockito-core", version.ref = "mockito4" }
mockito4inline = { module = "org.mockito:mockito-inline", version.ref = "mockito4" }
mockito5 = "org.mockito:mockito-core:5.6.0"
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 @@ -55,6 +55,16 @@ public Object createMock(@Nullable String name, Object instance, Type type, Mock
return 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 @@ -291,17 +301,78 @@ Object GroovySpyImpl(String inferredName, Class<?> inferredType, Map<String, Obj
}

private Object createMockImpl(String inferredName, Class<?> inferredType, MockNature nature,
MockImplementation implementation, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
MockImplementation implementation, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
return createMockImpl(inferredName, inferredType, null, nature, implementation, options, specifiedType, closure);
}

private Object createMockImpl(String inferredName, Class<?> inferredType, Object instance, MockNature nature,
MockImplementation implementation, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
MockImplementation implementation, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
Type effectiveType = specifiedType != null ? specifiedType : options.containsKey("type") ? (Type) options.get("type") : inferredType;
if (effectiveType == null) {
throw new InvalidSpecException("Mock object type cannot be inferred automatically. " +
"Please specify a type explicitly (e.g. 'Mock(Person)').");
"Please specify a type explicitly (e.g. 'Mock(Person)').");
}
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
Expand Up @@ -106,7 +106,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

0 comments on commit ef30dfe

Please sign in to comment.