Skip to content

joke/spock-mockable

Repository files navigation

spock-mockable

badge spock mockable stable spock mockable Conventional%20Commits 1.0.0 yellow pre-commit

spock-mockable allows creation of mocks otherwise un-mockable by the 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 How does it work.

  • Changes method visibility private to protected

  • Removes final from classes and methods

  • Supports mocking static methods

  • Automatically attaches java agent

  • Re-defines classes via Byte Buddy transformation.

  • Working with Spock Framework 2.0, 2.1, 2.2 and 2.3

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

stable

build.gradle
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
<depenencies>
  <dependency>
    <groupId>io.github.joke</groupId>
    <artifactId>spock-mockable</artifactId>
    <version>x.y.z</version>
    <scope>test</scope>
  </dependency>
</depenencies>

Usage

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

Mocks & Stubs

Mock definition
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
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
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 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 { callRealMethod*() }.

Note
Specifications defining interactions with static mocks are automatically annotated with @Isolated.
Mock static methods
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 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
@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.