diff --git a/docs/extensions.adoc b/docs/extensions.adoc index 2198faa0f3..87ecd5b72f 100644 --- a/docs/extensions.adoc +++ b/docs/extensions.adoc @@ -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 -<> and <> 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 <> + +Spring and Guice dependency injection is automatically handled by the +<> and <>, respectively. === AutoCleanup diff --git a/docs/interaction_based_testing.adoc b/docs/interaction_based_testing.adoc index 92a16a5bc3..d4f72e4678 100644 --- a/docs/interaction_based_testing.adoc +++ b/docs/interaction_based_testing.adoc @@ -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 <>. + 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: diff --git a/docs/module_spring.adoc b/docs/module_spring.adoc index f2548e89c8..606eae9bab 100644 --- a/docs/module_spring.adoc +++ b/docs/module_spring.adoc @@ -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. diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index 3d9c19604b..e651ff4629 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -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) diff --git a/spock-specs/src/test/groovy/org/spockframework/docs/interaction/DetachedMockFactoryDocSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/docs/interaction/DetachedMockFactoryDocSpec.groovy new file mode 100644 index 0000000000..c56645f147 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/docs/interaction/DetachedMockFactoryDocSpec.groovy @@ -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[] +}