Skip to content

Commit

Permalink
Documentation for DetachedMockFactory (#1728)
Browse files Browse the repository at this point in the history
Add a new section "Create Mock Objects outside Specifications" in the
interaction_based_testing.adoc to document usage of the
DetachedMockFactory.
Add reference to the new section to AutoAttach.

This fixes #1057

---------

Co-authored-by: Alexander Kriegisch <[email protected]>
Co-authored-by: Leonard Brünings <[email protected]>
  • Loading branch information
3 people authored Jan 24, 2024
1 parent dd798eb commit 0b5c4b5
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 3 deletions.
10 changes: 7 additions & 3 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -497,9 +497,13 @@ NOTE: This will acquire a `READ_WRITE` lock for `Resources.SYSTEM_PROPERTIES`, s

=== AutoAttach

Automatically attaches a detached mock to the current `Specification`. Use this if there is no direct framework
support available. 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.
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.

=== AutoCleanup

Expand Down
77 changes: 77 additions & 0 deletions docs/interaction_based_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,83 @@ mock.type == List
mock.nature == MockNature.MOCK
----

[[DetachedMockFactory]]
=== Create Mocks Outside Specifications

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: 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.
Spring and Guice dependency injection will also automatically attach mocks, when the `SpringExtension` or `GuiceExtension` is used.

Assuming that we have an application class `Car` and each car has an `Engine`:

[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 (preferably) shared or (suboptimally) static instances:

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

==== Manually attach / detach

Now in our feature, we create a detached `Engine` mock, attach the mock to the specification manually, stub its `isStarted()` method, 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[tags=attach-manually]
----

==== 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.

A valid use case could be a detached mock used in a dependency injection framework like Spring or Guice.
The framework might wire the mock and rely on some default behaviour, before Spock even gets a chance to stub any methods.
Defining default behaviour is possible using a combination of Spock's mocking API and custom `IDefaultResponse` implementations.

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[tags=custom-mock-creator]
----

The first parametrized feature uses the mock without attach:

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

The next parametrized feature method showcasing a set of usage scenarios when a detached mock
is attached to a spec before usage:

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

== Further Reading

If you would like to dive deeper into interaction-based testing, we recommend the following resources:
Expand Down
1 change: 1 addition & 0 deletions docs/module_spring.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ include::include.adoc[]
The Spring module enables integration with https://docs.spring.io/spring/docs/4.1.5.RELEASE/spring-framework-reference/html/testing.html#testcontext-framework[Spring TestContext Framework].
It supports the following spring annotations `@ContextConfiguration` and `@ContextHierarchy`. Furthermore, it supports the meta-annotation `@BootstrapWith` and so any annotation that is annotated with `@BootstrapWith` will also work, such as `@SpringBootTest`, `@WebMvcTest`. Please add dependency https://search.maven.org/artifact/org.spockframework/spock-spring[`org.spockframework:spock-spring`] to your project.

[[SpringMocks]]
== Mocks

Spock 1.1 introduced the `DetachedMockFactory` and the `SpockMockFactoryBean` which allow the creation of Spock mocks outside of a specification.
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ include::include.adoc[]
* Clarify documentation for global Mocks spockPull:1755[]
* Spock-Compiler does not use wrapper types anymore spockPull:1765[]
* Reduce lock contention of the `byte-buddy` mock maker, when multiple mocks are created concurrently spockPull:1778[]
* Documentation for DetachedMockFactory spockPull:1728[]

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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.Shared
import spock.lang.Specification
import spock.lang.Unroll
import spock.mock.AutoAttach
import spock.mock.DetachedMockFactory

import java.util.concurrent.ThreadLocalRandom

import static org.spockframework.docs.interaction.DetachedMockFactoryDocSpec.EngineMockCreator.StartMode.ALWAYS_STARTED
import static org.spockframework.docs.interaction.DetachedMockFactoryDocSpec.EngineMockCreator.StartMode.ALWAYS_STOPPED
import static org.spockframework.docs.interaction.DetachedMockFactoryDocSpec.EngineMockCreator.StartMode.RANDOMLY_STARTED
import static org.spockframework.docs.interaction.DetachedMockFactoryDocSpec.EngineMockCreator.StartMode.REAL_RESPONSE

class DetachedMockFactoryDocSpec extends Specification {
// tag::declare-shared[]
@Shared
def mockFactory = new DetachedMockFactory()

@Shared
def mockUtil = new MockUtil()
// end::declare-shared[]

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

when:
car.drive()

then:
1 * manuallyAttachedEngine.start()
manuallyAttachedEngine.isStarted()

when:
car.park()

then:
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)

when:
car.drive()

then:
1 * autoAttachedEngine.start()
autoAttachedEngine.isStarted()

when:
car.park()

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

// tag::use-custom-mock-creator-no-attach[]
@Unroll("Engine state #engineStateResponseType")
def "Mock usage without manually attach detach with preconfigured engine state"() {
given:
def car = new Car(engine: preconfiguredEngine)
// The preconfigured mock with default behaviour behaves as defined,
// even *without* attaching it to the spec.

when:
car.drive()

then:
possibleResponsesAfterStart.contains(preconfiguredEngine.isStarted())

when:
car.park()

then:
possibleResponsesAfterStop.contains(preconfiguredEngine.isStarted())

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-no-attach[]


// tag::use-custom-mock-creator-attach[]
@Unroll("Engine state #engineStateResponseType")
def "Manually attach detached mock with preconfigured engine state"() {
given:
def car = new Car(engine: preconfiguredEngine)
//Now, let's attach the mock to the spec and override its default behaviour.
mockUtil.attachMock(preconfiguredEngine, this)
preconfiguredEngine.isStarted() >> true

expect:
preconfiguredEngine.isStarted()
// The attached mock now behaves differently. Because it has been attached to the
// spec, we can also verify interactions using '1 * ...' or similar, which
// would not be possible without attaching it.

when:
car.drive()

then:
1 * preconfiguredEngine.start()
preconfiguredEngine.isStarted()

when:
car.park()

then:
1 * preconfiguredEngine.stop()
preconfiguredEngine.isStarted()

cleanup:
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-attach[]

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
}

static DetachedMockFactory mockFactory = new DetachedMockFactory()

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
? ThreadLocalRandom.current().nextBoolean()
: startMode == ALWAYS_STARTED
}
}

static Engine getMock(StartMode startMode) {
startMode == REAL_RESPONSE
? mockFactory.Spy(new Engine())
: mockFactory.Mock(Engine, defaultResponse: new EngineStateResponse(startMode: startMode)) as Engine
}
}
// end::custom-mock-creator[]
}

0 comments on commit 0b5c4b5

Please sign in to comment.