Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation for DetachedMockFactory #1728

Merged
merged 13 commits into from
Jan 24, 2024
Merged
Prev Previous commit
Next Next commit
Reword and extend detached mock docs and demo spec
kriegaex authored and AndreasTu committed Oct 1, 2023
commit 35487f11a4b6d19fbbe6cd869168e3fd33953d10
10 changes: 5 additions & 5 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
@@ -498,13 +498,13 @@ NOTE: This will acquire a `READ_WRITE` lock for `Resources.SYSTEM_PROPERTIES`, s
[[AutoAttach]]
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
=== AutoAttach

Automatically attaches a detached mock to the current `Specification`.
The `@AutoAttach` can only be used on non-shared/non-static fields.
Use this if there is no direct framework support available.
To create detached mocks, see <<interaction_based_testing.adoc#DetachedMockFactory,Create Mock Objects outside Specifications>>
Automatically attaches a detached mock to the current specification.
`@AutoAttach` can only be used regular instance fields, not on shared or static ones.
Use this, if there is no direct framework support available.
To create detached mocks, see <<interaction_based_testing.adoc#DetachedMockFactory,Create Mocks Outside Specifications>>

Spring and Guice dependency injection is automatically handled by the
<<module_spring.adoc#_spring_module,Spring Module>> and <<modules.adoc#_guice_module,Guice Module>> respectively.
<<module_spring.adoc#_spring_module,Spring Module>> and <<modules.adoc#_guice_module,Guice Module>>, respectively.

=== AutoCleanup

60 changes: 42 additions & 18 deletions docs/interaction_based_testing.adoc
Original file line number Diff line number Diff line change
@@ -1091,41 +1091,65 @@ mock.nature == MockNature.MOCK
----

[[DetachedMockFactory]]
=== Create Mock Objects outside Specifications
=== Create Mocks Outside Specifications

It can be sometimes helpful to create mock, stub or spy objects outside a `Specification`, but use them in a `Specification`.
The `spock.mock.DetachedMockFactory` provides API to create such mock objects. See <<module_spring.adoc#SpringMocks,Spring Mocks>> for a usage sample.
Sometimes, it can be helpful to create and possibly preconfigure mock, stub or spy objects outside specifications, especially if you want to factor out duplicate or similar code from there.
Use `DetachedMockFactory` to create such mock objects.

NOTE: Mocks created by the `DetachedMockFactory` must be manually attached to the `Specification`
using `MockUtil.attachMock(Object, Specification)` and detached afterwards using `MockUtil.detachMock(Object)`.
+
You could also use the `@AutoAttach` annotation on fields, see <<extensions.adoc#AutoAttach,AutoAttach>>.
NOTE: Detached mocks, as the name implies, must be attached to a specification to make them functional.
This either happens manually by calling `MockUtil.attachMock(Object, Specification)` or automatically by declaring the mock as an instance field with an `@AutoAttach` annotation, see <<extensions.adoc#AutoAttach,AutoAttach>>.
While auto-attached mocks will also be auto-detached during specification clean-up, you need to take care of manually attached mocks by yourself, calling `MockUtil.detachMock(Object)` when they are no longer in use.

NOTE: Although the mocks can be created outside of a specification, they only work properly inside the scope of a `Specification`.
All interactions with them until they are attached to a `Specification`, are handled by the default behavior and **not** recorded.
+
Furthermore, mocks can only be attached to one `Specification` instance at a time so keep that in mind when using multi-threaded executions.
Assuming that we have an application class `Car` and each car has an `Engine`:

To create a mock you use the `DetachedMockFactory`:
[source,groovy,indent=0]
----
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tags=engine;car]
----

In our specification, we declare a detached mock factory and a mock util, either inside a feature method or, if we need them for multiple features, as shared or static instances:

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tags=declare-static]
----

==== Manually attach / detach

Now in our feature, we create a detached `Engine` mock, stub its `isStarted()` method, attach the mock to the specification manually, inject the mock into a `Car` subject under specification, use the subject and finally detach the mock again after use:

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tag=DetachedMockFactory-usage-spec]
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tags=attach-manually]
----

In the `Specification` you can use the mock, after you attached it:
==== Use @AutoAttach

Same situation, different approach: We let Spock take care of automatically attaching the detached mock when starting the feature method and detaching it again after running the feature.

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tags=auto-attach]
----

==== Pre-configure detached mock with default response

This advanced use case might only be of interest to a minority of users, you may skip it for now if you just want to learn Spock basics.

Usually, you would simply create your detached mock outside of Spock, but then define interactions (stubbed method results, method call verifications) inside the feature method itself. Sometimes however, you might want to define some (user-overridable) default behaviour right in the detached mock definition itself and even make it somewhat configurable. This is possible using a combination of Spock's mocking API and custom `IDefaultResponse` implementations.
leonard84 marked this conversation as resolved.
Show resolved Hide resolved

A custom detached mock creator could look like this (please figure out by yourself what it does and how it works):

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tag=attach-usage]
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tags=custom-mock-creator]
----

You could also change the default behavior of a detached mock with your own implementation of an `IDefaultResponse`.
This allows you to control the default response also outside of a `Specification`.
A parametrized feature method showcasing a set of usage scenarios goes as follows:

[source,groovy,indent=0]
----
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tag=defaultResponse-usage]
include::{sourcedir}/interaction/DetachedMockFactoryDocSpec.groovy[tags=use-custom-mock-creator]
----

== Further Reading
Original file line number Diff line number Diff line change
@@ -3,65 +3,147 @@ package org.spockframework.docs.interaction
import org.spockframework.mock.IDefaultResponse
import org.spockframework.mock.IMockInvocation
import org.spockframework.mock.MockUtil
import org.spockframework.mock.ZeroOrNullResponse
import spock.lang.Specification
import spock.lang.Unroll
import spock.mock.AutoAttach
import spock.mock.DetachedMockFactory

import static org.spockframework.docs.interaction.DetachedMockFactoryDocSpec.EngineMockCreator.StartMode.*

class DetachedMockFactoryDocSpec extends Specification {
// tag::declare-static[]
static mockFactory = new DetachedMockFactory()
static mockUtil = new MockUtil()
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
// end::declare-static[]

// tag::attach-manually[]
def "Manually attach detached mock"() {
given:
def manuallyAttachedEngine = mockFactory.Mock(Engine)
manuallyAttachedEngine.isStarted() >> true
mockUtil.attachMock(manuallyAttachedEngine, this)
def car = new Car(engine: manuallyAttachedEngine)

when:
car.drive()
then:
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
1 * manuallyAttachedEngine.start()
manuallyAttachedEngine.isStarted()

def "Create a mock with DetachedMockFactory"() {
setup:
def mockUtil = new MockUtil()
when:
// tag::DetachedMockFactory-usage-spec[]
DetachedMockFactory factory = new DetachedMockFactory()
List listMock = factory.Mock(List.class)
List listStub = factory.Stub(ArrayList.class)
// end::DetachedMockFactory-usage-spec[]
car.park()
then:
listMock instanceof List
listStub instanceof ArrayList
mockUtil.isMock(listMock)
mockUtil.isMock(listStub)
1 * manuallyAttachedEngine.stop()
manuallyAttachedEngine.isStarted()

cleanup:
mockUtil.detachMock(manuallyAttachedEngine)
}
// end::attach-manually[]

// tag::auto-attach[]
@AutoAttach
def autoAttachedEngine = mockFactory.Mock(Engine)

def "Auto-attach detached mock"() {
given:
autoAttachedEngine.isStarted() >> true
def car = new Car(engine: autoAttachedEngine)

def "Attach Mock"() {
DetachedMockFactory factory = new DetachedMockFactory()
List listMock = factory.Mock(List.class)
// tag::attach-usage[]
setup:
def mockUtil = new MockUtil()
mockUtil.attachMock(listMock, this)
when:
//Use the mock here
listMock.add(1)
car.drive()
then:
1 * listMock.add(_)
1 * autoAttachedEngine.start()
autoAttachedEngine.isStarted()

when:
car.park()
then:
1 * autoAttachedEngine.stop()
autoAttachedEngine.isStarted()
}
// end::auto-attach[]

// tag::use-custom-mock-creator[]
@Unroll("Engine state #engineStateResponseType")
def "Manually attach detached mock with preconfigured engine state"() {
given:
mockUtil.attachMock(preconfiguredEngine, this)
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
def car = new Car(engine: preconfiguredEngine)

when:
car.drive()
then:
1 * preconfiguredEngine.start()
possibleResponsesAfterStart.contains(preconfiguredEngine.isStarted())

when:
car.park()
then:
1 * preconfiguredEngine.stop()
possibleResponsesAfterStop.contains(preconfiguredEngine.isStarted())

cleanup:
mockUtil.detachMock(listMock)
// end::attach-usage[]
mockUtil.detachMock(preconfiguredEngine)

where:
engineStateResponseType | possibleResponsesAfterStart | possibleResponsesAfterStop
ALWAYS_STARTED | [true] | [true]
ALWAYS_STOPPED | [false] | [false]
RANDOMLY_STARTED | [true, false] | [true, false]
REAL_RESPONSE | [true] | [false]
preconfiguredEngine = EngineMockCreator.getMock(engineStateResponseType)
}
// end::use-custom-mock-creator[]

static
// tag::engine[]
class Engine {
private boolean started

boolean isStarted() { return started }
void start() { started = true }
void stop() { started = false }
}

// end::engine[]

static
// tag::car[]
class Car {
private Engine engine

void drive() { engine.start() }
void park() { engine.stop() }
}
// end::car[]

static
// tag::custom-mock-creator[]
class EngineMockCreator {
enum StartMode {
ALWAYS_STARTED, ALWAYS_STOPPED, RANDOMLY_STARTED, REAL_RESPONSE
}

// tag::defaultResponse-usage[]
class CustomResponse implements IDefaultResponse {
Object respond(IMockInvocation invocation) {
if (invocation.method.name == "get") {
return "value"
static class EngineStateResponse implements IDefaultResponse {
StartMode startMode

@Override
Object respond(IMockInvocation invocation) {
if (invocation.method.name != 'isStarted')
return ZeroOrNullResponse.INSTANCE.respond(invocation)
startMode == RANDOMLY_STARTED
? new Random().nextBoolean()
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
: startMode == ALWAYS_STARTED
}
return null
}

static Engine getMock(StartMode startMode) {
startMode == REAL_RESPONSE
? mockFactory.Spy(new Engine())
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
: mockFactory.Mock(Engine, defaultResponse: new EngineStateResponse(startMode: startMode)) as Engine
}
}
DetachedMockFactory factory = new DetachedMockFactory()
List yourMock = factory.Mock(List, defaultResponse: new CustomResponse())
// end::defaultResponse-usage[]

def "Usage of an IDefaultResponse in a detached mock"() {
setup:
def mockUtil = new MockUtil()
mockUtil.attachMock(yourMock, this)
expect:
yourMock.get(0) == "value"
cleanup:
mockUtil.detachMock(yourMock)
}
// end::custom-mock-creator[]
}