Skip to content
Draft
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
13 changes: 13 additions & 0 deletions docs/interaction_based_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,19 @@ include::{sourcedir}/interaction/ExternalMockInteractionsDocSpec.groovy[tags=sup

A class cannot be both a `Specification` and a `MockInteractionSupport`, because a spec already has the full capability.

Instead of passing the spec through a constructor, a fixture can implement `SpecificationAttachable` and be wired up with `@AutoAttach`.
The extension injects the running spec into the fixture during `setup` and detaches it during `cleanup`:

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/ExternalMockInteractionsDocSpec.groovy[tags=autoattach-fixture]
----

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/ExternalMockInteractionsDocSpec.groovy[tags=autoattach-usage]
----

If you forget to attach the fixture, using it fails fast with a clear error that tells you the spec is missing.

[[Interactions]]
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include::include.adoc[]
* Add support for `final` local variables in `where:` blocks, declared at their beginning and evaluated once per feature, scoped to the where-block spockIssue:138[]
* Improve `TooManyInvocationsError` now reports unsatisfied interactions with argument mismatch details, making it easier to diagnose why invocations didn't match expected interactions spockPull:2315[]
* Allow declaring mock interactions outside a `Specification` via the `MockInteractionSupport` interface, the `@Interactions` method annotation, and `@SelfType(Specification)` traits, see <<interaction_based_testing.adoc#external-interactions,Declaring Interactions Outside Specifications>>
* `@AutoAttach` now also attaches fields implementing `spock.mock.SpecificationAttachable` (promoted from the internal `org.spockframework.mock.runtime` package, where a deprecated alias remains), so a `MockInteractionSupport` fixture can receive the running spec automatically

=== Misc

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

package org.spockframework.mock;

import org.spockframework.mock.runtime.SpecificationAttachable;
import spock.mock.SpecificationAttachable;
import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;
import spock.lang.Specification;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.util.ReflectionUtil;
import spock.lang.Specification;
import spock.mock.SpecificationAttachable;

import java.lang.reflect.*;
import java.util.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.lang.reflect.Method;

import org.spockframework.mock.IResponseGenerator;
import spock.mock.SpecificationAttachable;

public interface IProxyBasedMockInterceptor extends SpecificationAttachable {
Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.spockframework.runtime.InvalidSpecException;
import org.spockframework.util.Nullable;
import spock.lang.Specification;
import spock.mock.SpecificationAttachable;

import java.lang.reflect.Type;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
package org.spockframework.mock.runtime;

import spock.lang.Specification;

/**
* The Object implementing this interface can be attached to and
* detached from a {@link Specification}.
* detached from a {@link spock.lang.Specification}.
*
* @author Leonard Bruenings
* @deprecated implement {@link spock.mock.SpecificationAttachable} instead;
* this internal alias remains only for binary compatibility.
*/
public interface SpecificationAttachable {

/**
* Attaches the mock to a specification.
*
* @param specification specification that this mock object should attached to
*/
void attach(Specification specification);

/**
* Detaches the mock from its current specification.
*/
void detach();

@Deprecated
public interface SpecificationAttachable extends spock.mock.SpecificationAttachable {
}
4 changes: 3 additions & 1 deletion spock-core/src/main/java/spock/mock/AutoAttach.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import org.spockframework.runtime.extension.ExtensionAnnotation;

/**
* Automatically attaches detached mocks {@link DetachedMockFactory} to a Specification.
* Automatically attaches detached mocks {@link DetachedMockFactory} and
* {@link SpecificationAttachable} fixtures to a Specification. The field value
* is attached during {@code setup} and detached during {@code cleanup}.
*
* This can be used if there is no explicit framework support for attaching detached mocks.
* It does not work for {@code static} or {@code @Shared} fields.
Expand Down
29 changes: 21 additions & 8 deletions spock-core/src/main/java/spock/mock/AutoAttachExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,23 @@ private AttachInterceptor(FieldInfo field) {

@Override
public void intercept(IMethodInvocation invocation) throws Throwable {
Object mock = field.readValue(invocation.getInstance());
if (mock == null) {
Object value = field.readValue(invocation.getInstance());
if (value == null) {
throw new SpockException(String.format(Locale.ROOT, "Cannot AutoAttach 'null' for field %s:%d",
field.getName(), field.getLine()));
}
if (!MOCK_UTIL.isMock(mock)) {
throw new SpockException(String.format(Locale.ROOT, "AutoAttach failed '%s' is not a mock for field %s:%d",
mock, field.getName(), field.getLine()));
Specification specification = (Specification) invocation.getInstance();
// check for a mock first: a mock of a type that implements
// SpecificationAttachable must be attached as a mock, not have the
// attach() call swallowed by its own interceptor
if (MOCK_UTIL.isMock(value)) {
MOCK_UTIL.attachMock(value, specification);
} else if (value instanceof SpecificationAttachable) {
((SpecificationAttachable) value).attach(specification);
} else {
throw new SpockException(String.format(Locale.ROOT, "AutoAttach failed '%s' is neither a mock nor a SpecificationAttachable for field %s:%d",
value, field.getName(), field.getLine()));
}
MOCK_UTIL.attachMock(mock, (Specification) invocation.getInstance());
invocation.proceed();
}
}
Expand All @@ -67,8 +74,14 @@ private DetachInterceptor(FieldInfo field) {

@Override
public void intercept(IMethodInvocation invocation) throws Throwable {
Object mock = field.readValue(invocation.getInstance());
MOCK_UTIL.detachMock(mock);
Object value = field.readValue(invocation.getInstance());
if (MOCK_UTIL.isMock(value)) {
MOCK_UTIL.detachMock(value);
} else if (value instanceof SpecificationAttachable) {
((SpecificationAttachable) value).detach();
}
// a value that is neither already failed the attach interceptor with a
// clear error; nothing to detach then
invocation.proceed();
}
}
Expand Down
47 changes: 47 additions & 0 deletions spock-core/src/main/java/spock/mock/SpecificationAttachable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2026 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 spock.mock;

import org.spockframework.util.Beta;
import spock.lang.Specification;

/**
* An object that can be attached to and detached from a {@link Specification}.
*
* <p>Spock's mock objects implement this interface internally. A user class
* (for example a {@link MockInteractionSupport} fixture) may implement it to
* receive the running spec from the {@link AutoAttach} extension, which calls
* {@link #attach} during {@code setup} and {@link #detach} during
* {@code cleanup}.
*
* @since 2.5
*/
@Beta
public interface SpecificationAttachable {

/**
* Attaches this object to a specification.
*
* @param specification the specification this object should be attached to
*/
void attach(Specification specification);

/**
* Detaches this object from its current specification.
*/
void detach();
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
package org.spockframework.docs.interaction

import groovy.transform.SelfType
import spock.mock.SpecificationAttachable
import spock.lang.Specification
import spock.mock.AutoAttach
import spock.mock.MockInteractionSupport
import spock.lang.Interactions

class ExternalMockInteractionsDocSpec extends Specification {

// tag::autoattach-usage[]
@AutoAttach
AttachableOrderFixtures orderFixtures = new AttachableOrderFixtures()

def "@AutoAttach injects the running spec into a SpecificationAttachable fixture"() {
given:
PaymentGateway gateway = orderFixtures.happyGateway()

when:
boolean charged = gateway.charge(42)
gateway.audit("processed")

then:
charged
}
// end::autoattach-usage[]

// tag::support-fixture[]
static class OrderFixtures implements MockInteractionSupport {
final Specification specification
Expand Down Expand Up @@ -64,6 +83,28 @@ class ExternalMockInteractionsDocSpec extends Specification {
}
// end::interactions-usage[]

// tag::autoattach-fixture[]
static class AttachableOrderFixtures implements MockInteractionSupport, SpecificationAttachable {
private Specification specification

@Override
void attach(Specification specification) { this.specification = specification }

@Override
void detach() { this.specification = null }

@Override
Specification getSpecification() { specification }

PaymentGateway happyGateway() {
PaymentGateway gateway = Mock()
gateway.charge(42) >> true
1 * gateway.audit("processed")
return gateway
}
}
// end::autoattach-fixture[]

interface PaymentGateway {
boolean charge(int amount)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ class AutoAttachInvalidUsageSpec extends EmbeddedSpecification {
""")
then:
SpockException ex = thrown()
ex.message == "AutoAttach failed 'Value' is not a mock for field field:3"
ex.message == "AutoAttach failed 'Value' is neither a mock nor a SpecificationAttachable for field field:3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.spockframework.mock

import spock.mock.SpecificationAttachable
import spock.lang.Specification
import spock.mock.AutoAttach
import spock.mock.DetachedMockFactory

class AutoAttachSpecificationAttachableSpec extends Specification {

static class RecordingAttachable implements SpecificationAttachable {
Specification attached
int detachCount = 0

@Override
void attach(Specification specification) { attached = specification }

@Override
void detach() { detachCount++; attached = null }
}

@AutoAttach
RecordingAttachable fixture = new RecordingAttachable()

@AutoAttach
SpecificationAttachable detachedMock = new DetachedMockFactory().Mock(SpecificationAttachable)

def "AutoAttach attaches a SpecificationAttachable field to the running spec during setup"() {
expect:
fixture.attached.is(this)
}

def "a mock whose type implements SpecificationAttachable is attached as a mock, not via attach()"() {
when:
detachedMock.detach()

then: "the interaction is enforced on this spec, proving the mock was attached as a mock"
1 * detachedMock.detach()
}
}
Loading