joke / spock-mockable

Mock the un-mockable
Apache License 2.0
28 stars 2 forks source link
mocking spock-extension spock-framework

= spock-mockable :icons: font

image:https://github.com/joke/spock-mockable/workflows/build/badge.svg?branch=main[] image:https://badgen.net/github/license/joke/spock-mockable[] image:https://badgen.net/github/release/joke/spock-mockable/stable[] image:https://badgen.net/github/dependabot/joke/spock-mockable[] image:https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg[link=https://conventionalcommits.org] image:https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit[pre-commit, link=https://github.com/pre-commit/pre-commit]

spock-mockable allows creation of mocks otherwise un-mockable by the http://spockframework.org/[Spock Framework].

Spock is not capable of mocking private or final classes or methods because they are not accessible via inheritance. spock-mockable uses JVM instrumentation to modify these classes upon loading and apply looser access restrictions. In consequence Spock is capable of mocking these classes. Refer to <>.

== Setup

Add the artifact as an additional dependency to your spock setup.

TIP: Even though not strictly necessary it is recommended to register the dependency as a java agent with the JVM. This way more classes can be transformed.

=== Gradle

image:https://badgen.net/github/release/joke/spock-mockable/stable[]

.build.gradle [source,groovy]

dependencies { testImplementation 'io.github.joke:spock-mockable:x.y.z' }

// to load the agent even earlier add it as a JVM argument tasks.withType(Test) { jvmArgs += ["-javaagent:${classpath.find { it.name.contains('spock-mockable') }.absolutePath}"] }

=== Maven

.pom.xml [source,xml]

io.github.joke spock-mockable x.y.z test

== Usage

Under normal circumstances classes needing to undergo transformation are detected automatically.

=== Mocks & Stubs

.Mock definition [source,groovy]

class MySpec extends Specification { // either Person person = Mock() // detected class based on type of variable // or def person = Mock(Person) // detected class based on Mock parameter }

.Stub definition [source,groovy]

class MySpec extends Specification { // either Person person = Stub() // detected class based on type of variable // or def person = Stub(Person) // detected class based on Mock parameter }

=== Spies

.Spy definition [source,groovy]

class MySpec extends Specification { // either def person = new Person() Person personSpy = Spy(person) // detected class based on type of variable

// or
Person person = new Person()
def personSpy = Spy(personInstance) // detected class based on Mock parameter

// WARNING!
def person = new Person()
def person2 = personInstance // person2 is dynamic typed and ...
def personSpy = Spy(person2) // ... class type information is lost in this case!

}

=== Static methods

Static methods can be mocked in a similar fashion like https://spockframework.org/spock/docs/2.3/interaction_based_testing.html#_mocking_static_methods[mocking static methods using GroovySpy]. All classes which have undergone transformation are already prepared for mocking there static methods. The original method is called by default similar to a spy.

If an interaction has been registered within a feature method the real method is skipped and the interaction is executed.

A call of the method can also be forced by using https://spockframework.org/spock/docs/2.3/interaction_based_testing.html#Spies[{ callRealMethod*() }].

NOTE: Specifications defining interactions with static mocks are automatically annotated with https://spockframework.org/spock/docs/2.3/parallel_execution.html#_isolated_execution[`@Isolated`].

.Mock static methods [source,groovy]

class UtilitySpec extends Specification { def 'mock static method'() { setup: Spy(UtilityClass) // needed if class has not been mocked/spied/stubbed prior

    when:
    def res = UtilityClass.someStaticMethod('hello')

    then:
    1 * UtilityClass.someStaticMethod('hello') >> 'mock value'

    expect:
    res == 'mock value'
}

}

More examples can be found in link:examples[].

=== Transform special cases

In special cases you might want to manually specify additional classes or packages to undergo transformation. This need might arise if the exact class type can not be referenced in the specification. In this case you can specify arbitrary class or package names manually.

.Mockable annotation [source,groovy]

@Mockable(className = 'some.package.MyFirstClass') @Mockable(className = 'some.package.MySecondClass') @Mockable(packageName = 'some.package') class PersonSpec extends Specification { }

== How does it work

During groovy's compilation phase each specification is analyzed and mock invocations are detected. At the start of a test JVM these detected classes are transformed by the JVM instrumentation regardless of the actual specification there the mock invocation has been detected. This might lead to unexpected behaviour between different specifications.

For the earliest possible transformation of classes start the agent by using the JVM argument (-javaagent).

IMPORTANT: For agent instrumentation to work the JVM must support this feature. A JRE is most likely not sufficient.

IMPORTANT: The agent is attached to the JVM as early as possible but some classes not be transformed nevertheless because they are used prior. This restriction applies to some java.lang classes but also to some junit classes.

=== Conditionally disable transformation

You can disable spock-mockable by setting the JVM system property spock-mockable.disabled=true.